You've already forked golang-saas-starter-kit
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 'saas-starter-review'
# Conflicts: # cmd/web-app/README.md
This commit is contained in:
@ -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.
|
||||
|
||||
[](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/
|
||||
|
||||
[](https://api.example.saasstartupkit.com/docs/)
|
||||
|
||||
|
||||
|
||||
|
@ -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}"},
|
||||
|
@ -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 {
|
||||
|
49
cmd/web-api/handlers/example.go
Normal file
49
cmd/web-api/handlers/example.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -1,20 +1,25 @@
|
||||
# SaaS Web App
|
||||
|
||||
Copyright 2019, Geeks Accelerator
|
||||
accelerator@geeksinthewoods.com
|
||||
twins@geeksaccelerator.com
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
Responsive web application that renders HTML using the `html/template` package from the standard library to enable
|
||||
direct interaction with clients and their users. It allows clients to sign up new accounts and provides user
|
||||
authentication with HTTP sessions. To see screen captures of the web app, check out this Google Slides deck:
|
||||
authentication with HTTP sessions.
|
||||
|
||||
The web-app service is a fully functioning example. To see screen captures of the Golang web app, check out this Google
|
||||
Slides deck:
|
||||
https://docs.google.com/presentation/d/1WGYqMZ-YUOaNxlZBfU4srpN8i86MU0ppWWSBb3pkejM/edit#slide=id.p
|
||||
|
||||
*You are welcome to add comments to the Google Slides.*
|
||||
|
||||
[](https://docs.google.com/presentation/d/1WGYqMZ-YUOaNxlZBfU4srpN8i86MU0ppWWSBb3pkejM/edit#slide=id.p)
|
||||
We have also deployed this example Go web app to production here:
|
||||
https://example.saasstartupkit.com
|
||||
|
||||
[](https://example.saasstartupkit.com)
|
||||
|
||||
The web app relies on the Golang business logic packages developed to provide an API for internal requests.
|
||||
|
||||
@ -22,11 +27,122 @@ Once the web-app service is running it will be available on port 3000.
|
||||
|
||||
http://127.0.0.1:3000/
|
||||
|
||||
While the web-api service has
|
||||
significant functionality, this web-app service is still in development. Currently this web-app services only resizes
|
||||
an image and displays resized versions of it on the index page. See section below on Future Functionality.
|
||||
|
||||
If you would like to help, please email twins@geeksinthewoods.com.
|
||||
## Web App functionality
|
||||
|
||||
This example web app allows customers to subscribe to the SaaS. Once subscribed they can authenticate with the web app
|
||||
and the business value can be delivered as a service. The business value of the example web app allows users to manage
|
||||
projects. Users with access to the project can perform CRUD operations on the record.
|
||||
|
||||
This web-app service includes the following pages and corresponding functionality:
|
||||
|
||||
[](../../resources/images/saas-starter-kit-go-web-app-pages.png)
|
||||
|
||||
|
||||
### landing pages
|
||||
|
||||
The example web-app service in the SaaS Startup Kit includes typical pages for new customers to learn about your
|
||||
service. It allows new customers to review a pricing page as well as signup. Existing customers of your SaaS can login
|
||||
or connect with your support resources. The static web page for your SaaS website also includes a page for your web API
|
||||
service. These are working example pages that a typical SaaS product usually include.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-website-pricing.png)
|
||||
|
||||
|
||||
### signup
|
||||
|
||||
In order for your SaaS offering to deliver its value to your customer, they need to subscribe first. Users can subscribe
|
||||
using this signup page.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-website-signup.png)
|
||||
|
||||
The signup page creates an account and a user associated with the new account. This signup page
|
||||
also uses some cool inline validation.
|
||||
|
||||
|
||||
### authentication
|
||||
|
||||
Software-as-a-Service usually provides its service after a user has created an account and authenticated. After a user
|
||||
has an account, they can login to your web app. Once logged in they will have access to all pages that require
|
||||
authentication. This login page also uses some cool inline validation.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-website-login.png)
|
||||
|
||||
The GO web app implements Role-based access control (RBAC). The example web app has two basic roles for users: admin
|
||||
and user.
|
||||
* The role of admin provides the ability to perform all CRUD actions on projects and users.
|
||||
* The role of user limits users to only view projects and users.
|
||||
|
||||
Once a user is logged in, then RBAC is enforced and users only can access projects they have access to.
|
||||
|
||||
The web-app service also includes functionality for logout and forgot password. The forgot password functionality
|
||||
send an email to the user with a link to web page that allows them to change their password.
|
||||
|
||||
|
||||
### projects
|
||||
|
||||
The example code for the web-app service exposes business value to authenticated users. This business value is coded into
|
||||
various business logic packages. One example business logic package is the one to create and manage Projects. In the
|
||||
SaaS Startup Kit, projects represent the highest level of business value. Users can perform CRUD on project records.
|
||||
|
||||
The web app includes this index page that lists all records for projects. This index page uses Datatables to demonstrate
|
||||
providing advanced interactivity to HTML tables. This index page then allows users to view, update and delete an object.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-webapp-projects.png)
|
||||
|
||||
From the projects index page, users can click the button to create a new record. This create page demonstrates how a new
|
||||
record can be created for projects and also demonstrates inline validation.
|
||||
|
||||
The view page for an object displays the fields for the object as read-only. The page then includes links to edit or
|
||||
archive the object. The archive functionality demonstrates how a soft-delete can be performed. While the web app does
|
||||
not expose functionality to delete a record, the internal API does support the delete operation.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-webapp-project-view.png)
|
||||
|
||||
You can easily modify the projects package to support your own requirements. If you were providing a software-as-a-service
|
||||
similar to Github, Projects could be changed to be 'repositories'. If you were providing software-as-a-service similar
|
||||
to Slack, Projects could be modified to be 'channels', etc.
|
||||
|
||||
|
||||
### user (profile)
|
||||
|
||||
After users authenticate with the web app, there is example code for them to view their user details (view their profile).
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-webapp-profile-view2.png)
|
||||
|
||||
A user can then update the details for the record of their user. This another example demonstration the update operation.
|
||||
There is also functionality for the user to change their password.
|
||||
|
||||
|
||||
### account (management)
|
||||
|
||||
When a user signups to your SaaS via the web app, an account is created. Authenticated users can then view the details
|
||||
of their account.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-webapp-account-update2.png)
|
||||
|
||||
Users with role of admin can view and update the details of their account, while non-admins can only view the details
|
||||
of their account.
|
||||
|
||||
|
||||
### users (management)
|
||||
|
||||
Users with role of admin have access to functionality that allows them to manage the users associated with their account.
|
||||
This index page uses Datatables to demonstrate providing advanced interactivity to HTML tables.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-webapp-users.png)
|
||||
|
||||
From the users index page, users can access the functionality to create a new record. This create page demonstrates how
|
||||
a new record can be created for users. The create functionality also allows one or more roles to be applied for ACLs.
|
||||
|
||||
[](https://dzuyel7n94hma.cloudfront.net/img/saas-startup-example-golang-project-webapp-users-create.png)
|
||||
|
||||
If the admin would rather the new users provide their own user details, there is Go code demonstrating how users can be
|
||||
invited. The invite functionality allows users to specifiy one or more email addresses. Once submitted, the web app will
|
||||
send email invites to allow the users to activate their user.
|
||||
|
||||
From the users index page, admins for an account can view users details. This page also provides access to update the
|
||||
user as well as archive it.
|
||||
|
||||
|
||||
## Local Installation
|
||||
@ -40,7 +156,6 @@ go build .
|
||||
|
||||
To build using the docker file, need to be in the project root directory. `Dockerfile` references go.mod in root directory.
|
||||
|
||||
|
||||
```bash
|
||||
docker build -f cmd/web-app/Dockerfile -t saas-web-app .
|
||||
```
|
||||
@ -73,42 +188,4 @@ Test a specific language by appending the locale to the request URL.
|
||||
|
||||
### Future Functionality
|
||||
|
||||
This example Web App is going to allow users to manage checklists. Users with role of admin will be allowed to
|
||||
create new checklists (projects). Each checklist will have tasks (items) associated with it. Tasks can be assigned to
|
||||
users with access to the checklist. Users can then update the status of a task.
|
||||
|
||||
We are referring to "checklists" as "projects" and "tasks" as "items" so this example web-app service will be generic
|
||||
enough for you to leverage and build upon without lots of renaming.
|
||||
|
||||
The initial contributors to this project created a similar service like this: [standard operating procedure software](https://keeni.space/procedures/software)
|
||||
for Keeni.Space. Its' Golang web app for [standard operating procedures software](https://keeni.space/procedures/software) is available at [app.keeni.space](https://app.keeni.space) They plan on leveraging this experience and boil it down into a simplified set of functionality
|
||||
and corresponding web pages that will be a solid examples for building enterprise SaaS web apps with Golang.
|
||||
|
||||
This web-app service eventually will include the following:
|
||||
- authentication
|
||||
- signup (creates user and account records)
|
||||
- login
|
||||
- with role-based access
|
||||
- logout
|
||||
- forgot password
|
||||
- user management
|
||||
- update user and password
|
||||
- account management
|
||||
- update account
|
||||
- manage user
|
||||
- view user
|
||||
- create and invite user
|
||||
- update user
|
||||
- projects (checklists)
|
||||
- index of projects
|
||||
- browse, filter, search
|
||||
- manage projects
|
||||
- view project
|
||||
- with project items
|
||||
- create project
|
||||
- update project
|
||||
- user access
|
||||
- project items (tasks)
|
||||
- view item
|
||||
- create item (adds task to checklist)
|
||||
- update item
|
@ -34,7 +34,7 @@
|
||||
{"name": "ECS_SERVICE", "value": "{ECS_SERVICE}"},
|
||||
{"name": "WEB_APP_HTTP_HOST", "value": "{HTTP_HOST}"},
|
||||
{"name": "WEB_APP_HTTPS_HOST", "value": "{HTTPS_HOST}"},
|
||||
{"name": "WEB_APP_SERVICE_PROJECT", "value": "{APP_PROJECT}"},
|
||||
{"name": "WEB_APP_SERVICE_SERVICE_NAME", "value": "{SERVICE}"},
|
||||
{"name": "WEB_APP_SERVICE_BASE_URL", "value": "{APP_BASE_URL}"},
|
||||
{"name": "WEB_APP_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"},
|
||||
{"name": "WEB_APP_SERVICE_ENABLE_HTTPS", "value": "{HTTPS_ENABLED}"},
|
||||
@ -42,8 +42,9 @@
|
||||
{"name": "WEB_APP_SERVICE_STATICFILES_S3_PREFIX", "value": "{STATIC_FILES_S3_PREFIX}"},
|
||||
{"name": "WEB_APP_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", "value": "{STATIC_FILES_CLOUDFRONT_ENABLED}"},
|
||||
{"name": "WEB_APP_SERVICE_STATICFILES_IMG_RESIZE_ENABLED", "value": "{STATIC_FILES_IMG_RESIZE_ENABLED}"},
|
||||
{"name": "WEB_APP_SERVICE_EMAIL_SENDER", "value": "{EMAIL_SENDER}"},
|
||||
{"name": "WEB_APP_SERVICE_WEB_API_BASE_URL", "value": "{WEB_API_BASE_URL}"},
|
||||
{"name": "WEB_APP_PROJECT_PROJECT_NAME", "value": "{APP_PROJECT}"},
|
||||
{"name": "WEB_APP_PROJECT_EMAIL_SENDER", "value": "{EMAIL_SENDER}"},
|
||||
{"name": "WEB_APP_PROJECT_WEB_API_BASE_URL", "value": "{WEB_API_BASE_URL}"},
|
||||
{"name": "WEB_APP_REDIS_HOST", "value": "{CACHE_HOST}"},
|
||||
{"name": "WEB_APP_DB_HOST", "value": "{DB_HOST}"},
|
||||
{"name": "WEB_APP_DB_USER", "value": "{DB_USER}"},
|
||||
|
@ -2,9 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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/geonames"
|
||||
@ -12,16 +10,22 @@ import (
|
||||
"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"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Account represents the Account API method handler set.
|
||||
type Account struct {
|
||||
MasterDB *sqlx.DB
|
||||
Renderer web.Renderer
|
||||
Authenticator *auth.Authenticator
|
||||
AccountRepo handlers.AccountRepository
|
||||
AccountPrefRepo handlers.AccountPrefRepository
|
||||
AuthRepo handlers.UserAuthRepository
|
||||
GeoRepo GeoRepository
|
||||
Authenticator *auth.Authenticator
|
||||
Renderer web.Renderer
|
||||
}
|
||||
|
||||
// View handles displaying the current account profile.
|
||||
@ -35,7 +39,7 @@ func (h *Account) View(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
return err
|
||||
}
|
||||
|
||||
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience)
|
||||
acc, err := h.AccountRepo.ReadByID(ctx, claims, claims.Audience)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -77,7 +81,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
return false, err
|
||||
}
|
||||
|
||||
prefs, err := account_preference.FindByAccountID(ctx, claims, h.MasterDB, account_preference.AccountPreferenceFindByAccountIDRequest{
|
||||
prefs, err := h.AccountPrefRepo.FindByAccountID(ctx, claims, account_preference.AccountPreferenceFindByAccountIDRequest{
|
||||
AccountID: claims.Audience,
|
||||
})
|
||||
if err != nil {
|
||||
@ -115,7 +119,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
req.ID = claims.Audience
|
||||
|
||||
err = account.Update(ctx, claims, h.MasterDB, req.AccountUpdateRequest, ctxValues.Now)
|
||||
err = h.AccountRepo.Update(ctx, claims, req.AccountUpdateRequest, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -135,7 +139,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if preferenceDatetimeFormat != req.PreferenceDatetimeFormat {
|
||||
err = account_preference.Set(ctx, claims, h.MasterDB, account_preference.AccountPreferenceSetRequest{
|
||||
err = h.AccountPrefRepo.Set(ctx, claims, account_preference.AccountPreferenceSetRequest{
|
||||
AccountID: claims.Audience,
|
||||
Name: account_preference.AccountPreference_Datetime_Format,
|
||||
Value: req.PreferenceDatetimeFormat,
|
||||
@ -156,7 +160,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if preferenceDateFormat != req.PreferenceDateFormat {
|
||||
err = account_preference.Set(ctx, claims, h.MasterDB, account_preference.AccountPreferenceSetRequest{
|
||||
err = h.AccountPrefRepo.Set(ctx, claims, account_preference.AccountPreferenceSetRequest{
|
||||
AccountID: claims.Audience,
|
||||
Name: account_preference.AccountPreference_Date_Format,
|
||||
Value: req.PreferenceDateFormat,
|
||||
@ -177,7 +181,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if preferenceTimeFormat != req.PreferenceTimeFormat {
|
||||
err = account_preference.Set(ctx, claims, h.MasterDB, account_preference.AccountPreferenceSetRequest{
|
||||
err = h.AccountPrefRepo.Set(ctx, claims, account_preference.AccountPreferenceSetRequest{
|
||||
AccountID: claims.Audience,
|
||||
Name: account_preference.AccountPreference_Time_Format,
|
||||
Value: req.PreferenceTimeFormat,
|
||||
@ -213,7 +217,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
return true, web.Redirect(ctx, w, r, "/account", http.StatusFound)
|
||||
}
|
||||
|
||||
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience)
|
||||
acc, err := h.AccountRepo.ReadByID(ctx, claims, claims.Audience)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -244,14 +248,14 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
|
||||
data["account"] = acc.Response(ctx)
|
||||
|
||||
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
|
||||
data["timezones"], err = h.GeoRepo.ListTimezones(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
data["geonameCountries"] = geonames.ValidGeonameCountries(ctx)
|
||||
|
||||
data["countries"], err = geonames.FindCountries(ctx, h.MasterDB, "name", "")
|
||||
data["countries"], err = h.GeoRepo.FindCountries(ctx, "name", "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -8,14 +8,24 @@ import (
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
//"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||
)
|
||||
|
||||
// Check provides support for orchestration geo endpoints.
|
||||
type Geo struct {
|
||||
MasterDB *sqlx.DB
|
||||
Redis *redis.Client
|
||||
Redis *redis.Client
|
||||
GeoRepo GeoRepository
|
||||
}
|
||||
|
||||
type GeoRepository interface {
|
||||
FindGeonames(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.Geoname, error)
|
||||
FindGeonamePostalCodes(ctx context.Context, where string, args ...interface{}) ([]string, error)
|
||||
FindGeonameRegions(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.Region, error)
|
||||
FindCountries(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.Country, error)
|
||||
FindCountryTimezones(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.CountryTimezone, error)
|
||||
ListTimezones(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// GeonameByPostalCode...
|
||||
@ -39,7 +49,7 @@ func (h *Geo) GeonameByPostalCode(ctx context.Context, w http.ResponseWriter, r
|
||||
|
||||
where := strings.Join(filters, " AND ")
|
||||
|
||||
res, err := geonames.FindGeonames(ctx, h.MasterDB, "postal_code", where, args...)
|
||||
res, err := h.GeoRepo.FindGeonames(ctx, "postal_code", where, args...)
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
return web.RespondJsonError(ctx, w, err)
|
||||
@ -74,7 +84,7 @@ func (h *Geo) PostalCodesAutocomplete(ctx context.Context, w http.ResponseWriter
|
||||
|
||||
where := strings.Join(filters, " AND ")
|
||||
|
||||
res, err := geonames.FindGeonamePostalCodes(ctx, h.MasterDB, where, args...)
|
||||
res, err := h.GeoRepo.FindGeonamePostalCodes(ctx, where, args...)
|
||||
if err != nil {
|
||||
return web.RespondJsonError(ctx, w, err)
|
||||
}
|
||||
@ -101,7 +111,7 @@ func (h *Geo) RegionsAutocomplete(ctx context.Context, w http.ResponseWriter, r
|
||||
|
||||
where := strings.Join(filters, " AND ")
|
||||
|
||||
res, err := geonames.FindGeonameRegions(ctx, h.MasterDB, "state_name", where, args...)
|
||||
res, err := h.GeoRepo.FindGeonameRegions(ctx, "state_name", where, args...)
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
return web.RespondJsonError(ctx, w, err)
|
||||
@ -144,7 +154,7 @@ func (h *Geo) CountryTimezones(ctx context.Context, w http.ResponseWriter, r *ht
|
||||
|
||||
where := strings.Join(filters, " AND ")
|
||||
|
||||
res, err := geonames.FindCountryTimezones(ctx, h.MasterDB, "timezone_id", where, args...)
|
||||
res, err := h.GeoRepo.FindCountryTimezones(ctx, "timezone_id", where, args...)
|
||||
if err != nil {
|
||||
return web.RespondJsonError(ctx, w, err)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/handlers"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@ -12,17 +13,17 @@ 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/project"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||
)
|
||||
|
||||
// Projects represents the Projects API method handler set.
|
||||
type Projects struct {
|
||||
MasterDB *sqlx.DB
|
||||
Redis *redis.Client
|
||||
Renderer web.Renderer
|
||||
ProjectRepo handlers.ProjectRepository
|
||||
Redis *redis.Client
|
||||
Renderer web.Renderer
|
||||
}
|
||||
|
||||
func urlProjectsIndex() string {
|
||||
@ -73,7 +74,7 @@ func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
var v datatable.ColumnValue
|
||||
switch col.Field {
|
||||
case "id":
|
||||
v.Value = fmt.Sprintf("%d", q.ID)
|
||||
v.Value = fmt.Sprintf("%s", q.ID)
|
||||
case "name":
|
||||
v.Value = q.Name
|
||||
v.Formatted = fmt.Sprintf("<a href='%s'>%s</a>", urlProjectsView(q.ID), v.Value)
|
||||
@ -110,7 +111,7 @@ func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
loadFunc := func(ctx context.Context, sorting string, fields []datatable.DisplayField) (resp [][]datatable.ColumnValue, err error) {
|
||||
res, err := project.Find(ctx, claims, h.MasterDB, project.ProjectFindRequest{
|
||||
res, err := h.ProjectRepo.Find(ctx, claims, project.ProjectFindRequest{
|
||||
Where: "account_id = ?",
|
||||
Args: []interface{}{claims.Audience},
|
||||
Order: strings.Split(sorting, ","),
|
||||
@ -186,7 +187,7 @@ func (h *Projects) Create(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
req.AccountID = claims.Audience
|
||||
|
||||
usr, err := project.Create(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||
usr, err := h.ProjectRepo.Create(ctx, claims, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -251,7 +252,7 @@ func (h *Projects) View(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
|
||||
switch r.PostForm.Get("action") {
|
||||
case "archive":
|
||||
err = project.Archive(ctx, claims, h.MasterDB, project.ProjectArchiveRequest{
|
||||
err = h.ProjectRepo.Archive(ctx, claims, project.ProjectArchiveRequest{
|
||||
ID: projectID,
|
||||
}, ctxValues.Now)
|
||||
if err != nil {
|
||||
@ -276,7 +277,7 @@ func (h *Projects) View(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
return nil
|
||||
}
|
||||
|
||||
prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID)
|
||||
prj, err := h.ProjectRepo.ReadByID(ctx, claims, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -320,7 +321,7 @@ func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
req.ID = projectID
|
||||
|
||||
err = project.Update(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||
err = h.ProjectRepo.Update(ctx, claims, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -351,7 +352,7 @@ func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
return nil
|
||||
}
|
||||
|
||||
prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID)
|
||||
prj, err := h.ProjectRepo.ReadByID(ctx, claims, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -8,9 +8,8 @@ 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"
|
||||
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/project_route"
|
||||
"github.com/ikeikeikeike/go-sitemap-generator/v2/stm"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sethgrid/pester"
|
||||
"io/ioutil"
|
||||
@ -19,10 +18,9 @@ import (
|
||||
|
||||
// Root represents the Root API method handler set.
|
||||
type Root struct {
|
||||
MasterDB *sqlx.DB
|
||||
Renderer web.Renderer
|
||||
Sitemap *stm.Sitemap
|
||||
ProjectRoutes project_routes.ProjectRoutes
|
||||
Renderer web.Renderer
|
||||
Sitemap *stm.Sitemap
|
||||
ProjectRoute project_route.ProjectRoute
|
||||
}
|
||||
|
||||
// Index determines if the user has authentication and loads the associated page.
|
||||
@ -57,7 +55,7 @@ func (h *Root) SitePage(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
tmpName = "site-api.gohtml"
|
||||
|
||||
// http://127.0.0.1:3001/docs/doc.json
|
||||
swaggerJsonUrl := h.ProjectRoutes.ApiDocsJson()
|
||||
swaggerJsonUrl := h.ProjectRoute.ApiDocsJson()
|
||||
|
||||
// Load the json file from the API service.
|
||||
res, err := pester.Get(swaggerJsonUrl)
|
||||
@ -93,8 +91,8 @@ func (h *Root) SitePage(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
data["urlApiBaseUri"] = h.ProjectRoutes.WebApiUrl(doc.BasePath)
|
||||
data["urlApiDocs"] = h.ProjectRoutes.ApiDocs()
|
||||
data["urlApiBaseUri"] = h.ProjectRoute.WebApiUrl(doc.BasePath)
|
||||
data["urlApiDocs"] = h.ProjectRoute.ApiDocs()
|
||||
|
||||
case "/pricing":
|
||||
tmpName = "site-pricing.gohtml"
|
||||
@ -123,7 +121,7 @@ func (h *Root) RobotTxt(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
return web.RespondText(ctx, w, txt, http.StatusOK)
|
||||
}
|
||||
|
||||
sitemapUrl := h.ProjectRoutes.WebAppUrl("/sitemap.xml")
|
||||
sitemapUrl := h.ProjectRoute.WebAppUrl("/sitemap.xml")
|
||||
|
||||
txt := fmt.Sprintf("User-agent: *\nDisallow: /ping\nDisallow: /status\nDisallow: /debug/\nSitemap: %s", sitemapUrl)
|
||||
return web.RespondText(ctx, w, txt, http.StatusOK)
|
||||
|
@ -9,13 +9,23 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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/notify"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
|
||||
|
||||
//"geeks-accelerator/oss/saas-starter-kit/internal/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/ikeikeikeike/go-sitemap-generator/v2/stm"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||
@ -27,30 +37,59 @@ const (
|
||||
TmplContentErrorGeneric = "error-generic.gohtml"
|
||||
)
|
||||
|
||||
type AppContext struct {
|
||||
Log *log.Logger
|
||||
Env webcontext.Env
|
||||
MasterDB *sqlx.DB
|
||||
Redis *redis.Client
|
||||
UserRepo handlers.UserRepository
|
||||
UserAccountRepo handlers.UserAccountRepository
|
||||
AccountRepo handlers.AccountRepository
|
||||
AccountPrefRepo handlers.AccountPrefRepository
|
||||
AuthRepo handlers.UserAuthRepository
|
||||
SignupRepo handlers.SignupRepository
|
||||
InviteRepo handlers.UserInviteRepository
|
||||
ProjectRepo handlers.ProjectRepository
|
||||
GeoRepo GeoRepository
|
||||
Authenticator *auth.Authenticator
|
||||
StaticDir string
|
||||
TemplateDir string
|
||||
Renderer web.Renderer
|
||||
ProjectRoute project_route.ProjectRoute
|
||||
PreAppMiddleware []web.Middleware
|
||||
PostAppMiddleware []web.Middleware
|
||||
}
|
||||
|
||||
// API returns a handler for a set of routes.
|
||||
func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir, templateDir string, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, projectRoutes project_routes.ProjectRoutes, secretKey string, notifyEmail notify.Email, renderer web.Renderer, globalMids ...web.Middleware) http.Handler {
|
||||
func APP(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, renderer), 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, appCtx.Renderer),
|
||||
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...)
|
||||
|
||||
// Build a sitemap.
|
||||
sm := stm.NewSitemap(1)
|
||||
sm.SetVerbose(false)
|
||||
sm.SetDefaultHost(projectRoutes.WebAppUrl(""))
|
||||
sm.SetDefaultHost(appCtx.ProjectRoute.WebAppUrl(""))
|
||||
sm.Create()
|
||||
|
||||
smLocAddModified := func(loc stm.URL, filename string) {
|
||||
contentPath := filepath.Join(templateDir, "content", filename)
|
||||
contentPath := filepath.Join(appCtx.TemplateDir, "content", filename)
|
||||
|
||||
file, err := os.Stat(contentPath)
|
||||
if err != nil {
|
||||
@ -64,48 +103,48 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
||||
|
||||
// Register project management pages.
|
||||
p := Projects{
|
||||
MasterDB: masterDB,
|
||||
Redis: redis,
|
||||
Renderer: renderer,
|
||||
ProjectRepo: appCtx.ProjectRepo,
|
||||
Redis: appCtx.Redis,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("POST", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/projects/create", p.Create, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects/create", p.Create, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
|
||||
// Register user management pages.
|
||||
us := Users{
|
||||
MasterDB: masterDB,
|
||||
Redis: redis,
|
||||
Renderer: renderer,
|
||||
Authenticator: authenticator,
|
||||
ProjectRoutes: projectRoutes,
|
||||
NotifyEmail: notifyEmail,
|
||||
SecretKey: secretKey,
|
||||
UserRepo: appCtx.UserRepo,
|
||||
UserAccountRepo: appCtx.UserAccountRepo,
|
||||
AuthRepo: appCtx.AuthRepo,
|
||||
InviteRepo: appCtx.InviteRepo,
|
||||
GeoRepo: appCtx.GeoRepo,
|
||||
Redis: appCtx.Redis,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("POST", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/users/invite/:hash", us.InviteAccept)
|
||||
app.Handle("GET", "/users/invite/:hash", us.InviteAccept)
|
||||
app.Handle("POST", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
|
||||
// Register user management and authentication endpoints.
|
||||
u := User{
|
||||
MasterDB: masterDB,
|
||||
Renderer: renderer,
|
||||
Authenticator: authenticator,
|
||||
ProjectRoutes: projectRoutes,
|
||||
NotifyEmail: notifyEmail,
|
||||
SecretKey: secretKey,
|
||||
u := UserRepos{
|
||||
UserRepo: appCtx.UserRepo,
|
||||
UserAccountRepo: appCtx.UserAccountRepo,
|
||||
AccountRepo: appCtx.AccountRepo,
|
||||
AuthRepo: appCtx.AuthRepo,
|
||||
GeoRepo: appCtx.GeoRepo,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("POST", "/user/login", u.Login)
|
||||
app.Handle("GET", "/user/login", u.Login)
|
||||
@ -114,35 +153,39 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
||||
app.Handle("GET", "/user/reset-password/:hash", u.ResetConfirm)
|
||||
app.Handle("POST", "/user/reset-password", u.ResetPassword)
|
||||
app.Handle("GET", "/user/reset-password", u.ResetPassword)
|
||||
app.Handle("POST", "/user/update", u.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/update", u.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/account", u.Account, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/virtual-login/:user_id", u.VirtualLogin, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/user/virtual-login", u.VirtualLogin, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/user/virtual-login", u.VirtualLogin, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/user/virtual-logout", u.VirtualLogout, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/user/switch-account", u.SwitchAccount, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/switch-account", u.SwitchAccount, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/user", u.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user", u.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/user/update", u.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/update", u.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/account", u.Account, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/virtual-login/:user_id", u.VirtualLogin, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/user/virtual-login", u.VirtualLogin, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/user/virtual-login", u.VirtualLogin, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/user/virtual-logout", u.VirtualLogout, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/user/switch-account", u.SwitchAccount, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user/switch-account", u.SwitchAccount, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("POST", "/user", u.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
app.Handle("GET", "/user", u.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
|
||||
|
||||
// Register account management endpoints.
|
||||
acc := Account{
|
||||
MasterDB: masterDB,
|
||||
Renderer: renderer,
|
||||
Authenticator: authenticator,
|
||||
AccountRepo: appCtx.AccountRepo,
|
||||
AccountPrefRepo: appCtx.AccountPrefRepo,
|
||||
AuthRepo: appCtx.AuthRepo,
|
||||
Authenticator: appCtx.Authenticator,
|
||||
GeoRepo: appCtx.GeoRepo,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("POST", "/account/update", acc.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/account/update", acc.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/account", acc.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/account", acc.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/account/update", acc.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/account/update", acc.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("POST", "/account", acc.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
app.Handle("GET", "/account", acc.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
|
||||
|
||||
// Register user management and authentication endpoints.
|
||||
// Register signup endpoints.
|
||||
s := Signup{
|
||||
MasterDB: masterDB,
|
||||
Renderer: renderer,
|
||||
Authenticator: authenticator,
|
||||
SignupRepo: appCtx.SignupRepo,
|
||||
AuthRepo: appCtx.AuthRepo,
|
||||
GeoRepo: appCtx.GeoRepo,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
// This route is not authenticated
|
||||
app.Handle("POST", "/signup", s.Step1)
|
||||
@ -150,16 +193,16 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
||||
|
||||
// Register example endpoints.
|
||||
ex := Examples{
|
||||
Renderer: renderer,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("POST", "/examples/flash-messages", ex.FlashMessages, mid.AuthenticateSessionOptional(authenticator))
|
||||
app.Handle("GET", "/examples/flash-messages", ex.FlashMessages, mid.AuthenticateSessionOptional(authenticator))
|
||||
app.Handle("GET", "/examples/images", ex.Images, mid.AuthenticateSessionOptional(authenticator))
|
||||
app.Handle("POST", "/examples/flash-messages", ex.FlashMessages, mid.AuthenticateSessionOptional(appCtx.Authenticator))
|
||||
app.Handle("GET", "/examples/flash-messages", ex.FlashMessages, mid.AuthenticateSessionOptional(appCtx.Authenticator))
|
||||
app.Handle("GET", "/examples/images", ex.Images, mid.AuthenticateSessionOptional(appCtx.Authenticator))
|
||||
|
||||
// Register geo
|
||||
g := Geo{
|
||||
MasterDB: masterDB,
|
||||
Redis: redis,
|
||||
GeoRepo: appCtx.GeoRepo,
|
||||
Redis: appCtx.Redis,
|
||||
}
|
||||
app.Handle("GET", "/geo/regions/autocomplete", g.RegionsAutocomplete)
|
||||
app.Handle("GET", "/geo/postal_codes/autocomplete", g.PostalCodesAutocomplete)
|
||||
@ -168,17 +211,16 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
||||
|
||||
// Register root
|
||||
r := Root{
|
||||
MasterDB: masterDB,
|
||||
Renderer: renderer,
|
||||
ProjectRoutes: projectRoutes,
|
||||
Sitemap: sm,
|
||||
Renderer: appCtx.Renderer,
|
||||
ProjectRoute: appCtx.ProjectRoute,
|
||||
Sitemap: sm,
|
||||
}
|
||||
app.Handle("GET", "/api", r.SitePage)
|
||||
app.Handle("GET", "/pricing", r.SitePage)
|
||||
app.Handle("GET", "/support", r.SitePage)
|
||||
app.Handle("GET", "/legal/privacy", r.SitePage)
|
||||
app.Handle("GET", "/legal/terms", r.SitePage)
|
||||
app.Handle("GET", "/", r.Index, mid.AuthenticateSessionOptional(authenticator))
|
||||
app.Handle("GET", "/", r.Index, mid.AuthenticateSessionOptional(appCtx.Authenticator))
|
||||
app.Handle("GET", "/index.html", r.IndexHtml)
|
||||
app.Handle("GET", "/robots.txt", r.RobotTxt)
|
||||
app.Handle("GET", "/sitemap.xml", r.SitemapXml)
|
||||
@ -193,14 +235,14 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
||||
|
||||
// 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)
|
||||
|
||||
// Handle static files/pages. Render a custom 404 page when file not found.
|
||||
static := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
err := web.StaticHandler(ctx, w, r, params, staticDir, "")
|
||||
err := web.StaticHandler(ctx, w, r, params, appCtx.StaticDir, "")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
rmsg := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
|
||||
@ -209,7 +251,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
||||
err = weberror.NewError(ctx, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return web.RenderError(ctx, w, r, err, renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||
return web.RenderError(ctx, w, r, err, appCtx.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/handlers"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
@ -20,9 +22,11 @@ import (
|
||||
|
||||
// Signup represents the Signup API method handler set.
|
||||
type Signup struct {
|
||||
MasterDB *sqlx.DB
|
||||
Renderer web.Renderer
|
||||
Authenticator *auth.Authenticator
|
||||
SignupRepo handlers.SignupRepository
|
||||
AuthRepo handlers.UserAuthRepository
|
||||
GeoRepo GeoRepository
|
||||
MasterDB *sqlx.DB
|
||||
Renderer web.Renderer
|
||||
}
|
||||
|
||||
// Step1 handles collecting the first detailed needed to create a new account.
|
||||
@ -52,7 +56,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Execute the account / user signup.
|
||||
_, err = signup.Signup(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||
_, err = h.SignupRepo.Signup(ctx, claims, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
case account.ErrForbidden:
|
||||
@ -68,7 +72,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Authenticated the new user.
|
||||
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, user_auth.AuthenticateRequest{
|
||||
token, err := h.AuthRepo.Authenticate(ctx, user_auth.AuthenticateRequest{
|
||||
Email: req.User.Email,
|
||||
Password: req.User.Password,
|
||||
}, time.Hour, ctxValues.Now)
|
||||
@ -77,7 +81,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Add the token to the users session.
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -107,7 +111,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
|
||||
data["geonameCountries"] = geonames.ValidGeonameCountries(ctx)
|
||||
|
||||
data["countries"], err = geonames.FindCountries(ctx, h.MasterDB, "name", "")
|
||||
data["countries"], err = h.GeoRepo.FindCountries(ctx, "name", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -8,17 +8,17 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/geonames"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jmoiron/sqlx"
|
||||
@ -26,13 +26,15 @@ import (
|
||||
)
|
||||
|
||||
// User represents the User API method handler set.
|
||||
type User struct {
|
||||
MasterDB *sqlx.DB
|
||||
Renderer web.Renderer
|
||||
Authenticator *auth.Authenticator
|
||||
ProjectRoutes project_routes.ProjectRoutes
|
||||
NotifyEmail notify.Email
|
||||
SecretKey string
|
||||
type UserRepos struct {
|
||||
UserRepo handlers.UserRepository
|
||||
AuthRepo handlers.UserAuthRepository
|
||||
UserAccountRepo handlers.UserAccountRepository
|
||||
AccountRepo handlers.AccountRepository
|
||||
GeoRepo GeoRepository
|
||||
MasterDB *sqlx.DB
|
||||
Renderer web.Renderer
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func urlUserVirtualLogin(userID string) string {
|
||||
@ -46,7 +48,7 @@ type UserLoginRequest struct {
|
||||
}
|
||||
|
||||
// Login handles authenticating a user into the system.
|
||||
func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h UserRepos) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
ctxValues, err := webcontext.ContextValues(ctx)
|
||||
if err != nil {
|
||||
@ -75,7 +77,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Authenticated the user.
|
||||
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, user_auth.AuthenticateRequest{
|
||||
token, err := h.AuthRepo.Authenticate(ctx, user_auth.AuthenticateRequest{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
}, sessionTTL, ctxValues.Now)
|
||||
@ -97,7 +99,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Add the token to the users session.
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -134,7 +136,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Logout handles removing authentication for the user.
|
||||
func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
sess := webcontext.ContextSession(ctx)
|
||||
|
||||
@ -150,7 +152,7 @@ func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// ResetPassword allows a user to perform forgot password.
|
||||
func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
ctxValues, err := webcontext.ContextValues(ctx)
|
||||
if err != nil {
|
||||
@ -173,7 +175,7 @@ func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = user.ResetPassword(ctx, h.MasterDB, h.ProjectRoutes.UserResetPassword, h.NotifyEmail, *req, h.SecretKey, ctxValues.Now)
|
||||
_, err = h.UserRepo.ResetPassword(ctx, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -210,7 +212,7 @@ func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// ResetConfirm handles changing a users password after they have clicked on the link emailed.
|
||||
func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
resetHash := params["hash"]
|
||||
|
||||
@ -238,7 +240,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
// Append the query param value to the request.
|
||||
req.ResetHash = resetHash
|
||||
|
||||
u, err := user.ResetConfirm(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
|
||||
u, err := h.UserRepo.ResetConfirm(ctx, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
case user.ErrResetExpired:
|
||||
@ -257,7 +259,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Authenticated the user. Probably should use the default session TTL from UserLogin.
|
||||
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, user_auth.AuthenticateRequest{
|
||||
token, err := h.AuthRepo.Authenticate(ctx, user_auth.AuthenticateRequest{
|
||||
Email: u.Email,
|
||||
Password: req.Password,
|
||||
}, time.Hour, ctxValues.Now)
|
||||
@ -271,7 +273,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Add the token to the users session.
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -318,7 +320,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// View handles displaying the current user profile.
|
||||
func (h *User) View(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) View(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
data := make(map[string]interface{})
|
||||
f := func() error {
|
||||
@ -328,14 +330,14 @@ func (h *User) View(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
||||
return err
|
||||
}
|
||||
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, claims.Subject)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, claims.Subject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data["user"] = usr.Response(ctx)
|
||||
|
||||
usrAccs, err := user_account.FindByUserID(ctx, claims, h.MasterDB, claims.Subject, false)
|
||||
usrAccs, err := h.UserAccountRepo.FindByUserID(ctx, claims, claims.Subject, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -358,7 +360,7 @@ func (h *User) View(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// Update handles allowing the current user to update their profile.
|
||||
func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
ctxValues, err := webcontext.ContextValues(ctx)
|
||||
if err != nil {
|
||||
@ -388,7 +390,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
req.ID = claims.Subject
|
||||
|
||||
err = user.Update(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||
err = h.UserRepo.Update(ctx, claims, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -409,7 +411,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
pwdReq.ID = claims.Subject
|
||||
|
||||
err = user.UpdatePassword(ctx, claims, h.MasterDB, *pwdReq, ctxValues.Now)
|
||||
err = h.UserRepo.UpdatePassword(ctx, claims, *pwdReq, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -441,7 +443,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
return nil
|
||||
}
|
||||
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, claims.Subject)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, claims.Subject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -455,7 +457,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
data["user"] = usr.Response(ctx)
|
||||
|
||||
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
|
||||
data["timezones"], err = h.GeoRepo.ListTimezones(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -474,7 +476,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Account handles displaying the Account for the current user.
|
||||
func (h *User) Account(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) Account(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
data := make(map[string]interface{})
|
||||
f := func() error {
|
||||
@ -484,7 +486,7 @@ func (h *User) Account(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
return err
|
||||
}
|
||||
|
||||
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience)
|
||||
acc, err := h.AccountRepo.ReadByID(ctx, claims, claims.Audience)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -501,7 +503,7 @@ func (h *User) Account(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// VirtualLogin handles switching the scope of the context to another user.
|
||||
func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
ctxValues, err := webcontext.ContextValues(ctx)
|
||||
if err != nil {
|
||||
@ -551,7 +553,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Perform the account switch.
|
||||
tkn, err := user_auth.VirtualLogin(ctx, h.MasterDB, h.Authenticator, claims, *req, expires, ctxValues.Now)
|
||||
tkn, err := h.AuthRepo.VirtualLogin(ctx, claims, *req, expires, ctxValues.Now)
|
||||
if err != nil {
|
||||
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||
data["validationErrors"] = verr.(*weberror.Error)
|
||||
@ -565,7 +567,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
sess = webcontext.SessionUpdateAccessToken(sess, tkn.AccessToken)
|
||||
|
||||
// Read the account for a flash message.
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, tkn.UserID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, tkn.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -588,7 +590,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
return nil
|
||||
}
|
||||
|
||||
usrAccs, err := user_account.Find(ctx, claims, h.MasterDB, user_account.UserAccountFindRequest{
|
||||
usrAccs, err := h.UserAccountRepo.Find(ctx, claims, user_account.UserAccountFindRequest{
|
||||
Where: "account_id = ?",
|
||||
Args: []interface{}{claims.Audience},
|
||||
})
|
||||
@ -612,7 +614,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
userPhs = append(userPhs, "?")
|
||||
}
|
||||
|
||||
users, err := user.Find(ctx, claims, h.MasterDB, user.UserFindRequest{
|
||||
users, err := h.UserRepo.Find(ctx, claims, user.UserFindRequest{
|
||||
Where: fmt.Sprintf("id IN (%s)",
|
||||
strings.Join(userPhs, ", ")),
|
||||
Args: userIDs,
|
||||
@ -636,7 +638,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// VirtualLogout handles switching the scope back to the user who initiated the virtual login.
|
||||
func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
ctxValues, err := webcontext.ContextValues(ctx)
|
||||
if err != nil {
|
||||
@ -657,7 +659,7 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
|
||||
expires = time.Hour
|
||||
}
|
||||
|
||||
tkn, err := user_auth.VirtualLogout(ctx, h.MasterDB, h.Authenticator, claims, expires, ctxValues.Now)
|
||||
tkn, err := h.AuthRepo.VirtualLogout(ctx, claims, expires, ctxValues.Now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -667,11 +669,11 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
|
||||
|
||||
// Display a success message to verify the user has switched contexts.
|
||||
if claims.Subject != tkn.UserID && claims.Audience != tkn.AccountID {
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, tkn.UserID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, tkn.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acc, err := account.ReadByID(ctx, claims, h.MasterDB, tkn.AccountID)
|
||||
acc, err := h.AccountRepo.ReadByID(ctx, claims, tkn.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -680,7 +682,7 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
|
||||
fmt.Sprintf("You are now virtually logged back into account %s user %s.",
|
||||
acc.Response(ctx).Name, usr.Response(ctx).Name))
|
||||
} else if claims.Audience != tkn.AccountID {
|
||||
acc, err := account.ReadByID(ctx, claims, h.MasterDB, tkn.AccountID)
|
||||
acc, err := h.AccountRepo.ReadByID(ctx, claims, tkn.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -689,7 +691,7 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
|
||||
fmt.Sprintf("You are now virtually logged back into account %s.",
|
||||
acc.Response(ctx).Name))
|
||||
} else {
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, tkn.UserID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, tkn.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -710,7 +712,7 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// VirtualLogin handles switching the scope of the context to another user.
|
||||
func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
func (h *UserRepos) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
ctxValues, err := webcontext.ContextValues(ctx)
|
||||
if err != nil {
|
||||
@ -757,7 +759,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// Perform the account switch.
|
||||
tkn, err := user_auth.SwitchAccount(ctx, h.MasterDB, h.Authenticator, claims, *req, expires, ctxValues.Now)
|
||||
tkn, err := h.AuthRepo.SwitchAccount(ctx, claims, *req, expires, ctxValues.Now)
|
||||
if err != nil {
|
||||
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||
data["validationErrors"] = verr.(*weberror.Error)
|
||||
@ -771,7 +773,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
||||
sess = webcontext.SessionUpdateAccessToken(sess, tkn.AccessToken)
|
||||
|
||||
// Read the account for a flash message.
|
||||
acc, err := account.ReadByID(ctx, claims, h.MasterDB, tkn.AccountID)
|
||||
acc, err := h.AccountRepo.ReadByID(ctx, claims, tkn.AccountID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -794,7 +796,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
||||
return nil
|
||||
}
|
||||
|
||||
accounts, err := account.Find(ctx, claims, h.MasterDB, account.AccountFindRequest{
|
||||
accounts, err := h.AccountRepo.Find(ctx, claims, account.AccountFindRequest{
|
||||
Order: []string{"name"},
|
||||
})
|
||||
if err != nil {
|
||||
@ -816,7 +818,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// handleSessionToken persists the access token to the session for request authentication.
|
||||
func handleSessionToken(ctx context.Context, db *sqlx.DB, w http.ResponseWriter, r *http.Request, token user_auth.Token) error {
|
||||
func handleSessionToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token user_auth.Token) error {
|
||||
if token.AccessToken == "" {
|
||||
return errors.New("accessToken is required.")
|
||||
}
|
||||
|
@ -3,37 +3,39 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
||||
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/handlers"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Users represents the Users API method handler set.
|
||||
type Users struct {
|
||||
MasterDB *sqlx.DB
|
||||
Redis *redis.Client
|
||||
Renderer web.Renderer
|
||||
Authenticator *auth.Authenticator
|
||||
ProjectRoutes project_routes.ProjectRoutes
|
||||
NotifyEmail notify.Email
|
||||
SecretKey string
|
||||
UserRepo handlers.UserRepository
|
||||
AccountRepo handlers.AccountRepository
|
||||
UserAccountRepo handlers.UserAccountRepository
|
||||
AuthRepo handlers.UserAuthRepository
|
||||
InviteRepo handlers.UserInviteRepository
|
||||
GeoRepo GeoRepository
|
||||
MasterDB *sqlx.DB
|
||||
Redis *redis.Client
|
||||
Renderer web.Renderer
|
||||
}
|
||||
|
||||
func urlUsersIndex() string {
|
||||
@ -100,7 +102,7 @@ func (h *Users) Index(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
var v datatable.ColumnValue
|
||||
switch col.Field {
|
||||
case "id":
|
||||
v.Value = fmt.Sprintf("%d", q.ID)
|
||||
v.Value = fmt.Sprintf("%s", q.ID)
|
||||
case "name":
|
||||
if strings.TrimSpace(q.Name) == "" {
|
||||
v.Value = q.Email
|
||||
@ -144,7 +146,7 @@ func (h *Users) Index(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
loadFunc := func(ctx context.Context, sorting string, fields []datatable.DisplayField) (resp [][]datatable.ColumnValue, err error) {
|
||||
res, err := user_account.UserFindByAccount(ctx, claims, h.MasterDB, user_account.UserFindByAccountRequest{
|
||||
res, err := h.UserAccountRepo.UserFindByAccount(ctx, claims, user_account.UserFindByAccountRequest{
|
||||
AccountID: claims.Audience,
|
||||
Order: strings.Split(sorting, ","),
|
||||
})
|
||||
@ -232,7 +234,7 @@ func (h *Users) Create(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
usr, err := user.Create(ctx, claims, h.MasterDB, req.UserCreateRequest, ctxValues.Now)
|
||||
usr, err := h.UserRepo.Create(ctx, claims, req.UserCreateRequest, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -246,7 +248,7 @@ func (h *Users) Create(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
uaStatus := user_account.UserAccountStatus_Active
|
||||
_, err = user_account.Create(ctx, claims, h.MasterDB, user_account.UserAccountCreateRequest{
|
||||
_, err = h.UserAccountRepo.Create(ctx, claims, user_account.UserAccountCreateRequest{
|
||||
UserID: usr.ID,
|
||||
AccountID: claims.Audience,
|
||||
Roles: req.Roles,
|
||||
@ -282,7 +284,7 @@ func (h *Users) Create(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
return nil
|
||||
}
|
||||
|
||||
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
|
||||
data["timezones"], err = h.GeoRepo.ListTimezones(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -327,7 +329,7 @@ func (h *Users) View(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
|
||||
switch r.PostForm.Get("action") {
|
||||
case "archive":
|
||||
err = user.Archive(ctx, claims, h.MasterDB, user.UserArchiveRequest{
|
||||
err = h.UserRepo.Archive(ctx, claims, user.UserArchiveRequest{
|
||||
ID: userID,
|
||||
}, ctxValues.Now)
|
||||
if err != nil {
|
||||
@ -352,14 +354,14 @@ func (h *Users) View(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
return nil
|
||||
}
|
||||
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, userID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data["user"] = usr.Response(ctx)
|
||||
|
||||
usrAccs, err := user_account.FindByUserID(ctx, claims, h.MasterDB, userID, false)
|
||||
usrAccs, err := h.UserAccountRepo.FindByUserID(ctx, claims, userID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -425,7 +427,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
err = user.Update(ctx, claims, h.MasterDB, req.UserUpdateRequest, ctxValues.Now)
|
||||
err = h.UserRepo.Update(ctx, claims, req.UserUpdateRequest, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -439,7 +441,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
if req.Roles != nil {
|
||||
err = user_account.Update(ctx, claims, h.MasterDB, user_account.UserAccountUpdateRequest{
|
||||
err = h.UserAccountRepo.Update(ctx, claims, user_account.UserAccountUpdateRequest{
|
||||
UserID: userID,
|
||||
AccountID: claims.Audience,
|
||||
Roles: &req.Roles,
|
||||
@ -465,7 +467,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
pwdReq.ID = userID
|
||||
|
||||
err = user.UpdatePassword(ctx, claims, h.MasterDB, *pwdReq, ctxValues.Now)
|
||||
err = h.UserRepo.UpdatePassword(ctx, claims, *pwdReq, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -497,12 +499,12 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
return nil
|
||||
}
|
||||
|
||||
usr, err := user.ReadByID(ctx, claims, h.MasterDB, userID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, claims, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usrAcc, err := user_account.Read(ctx, claims, h.MasterDB, user_account.UserAccountReadRequest{
|
||||
usrAcc, err := h.UserAccountRepo.Read(ctx, claims, user_account.UserAccountReadRequest{
|
||||
UserID: userID,
|
||||
AccountID: claims.Audience,
|
||||
})
|
||||
@ -520,7 +522,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
|
||||
data["user"] = usr.Response(ctx)
|
||||
|
||||
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
|
||||
data["timezones"], err = h.GeoRepo.ListTimezones(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -577,7 +579,7 @@ func (h *Users) Invite(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
req.UserID = claims.Subject
|
||||
req.AccountID = claims.Audience
|
||||
|
||||
res, err := invite.SendUserInvites(ctx, claims, h.MasterDB, h.ProjectRoutes.UserInviteAccept, h.NotifyEmail, *req, h.SecretKey, ctxValues.Now)
|
||||
res, err := h.InviteRepo.SendUserInvites(ctx, claims, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
default:
|
||||
@ -661,7 +663,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
|
||||
// Append the query param value to the request.
|
||||
req.InviteHash = inviteHash
|
||||
|
||||
hash, err := invite.AcceptInviteUser(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
|
||||
hash, err := h.InviteRepo.AcceptInviteUser(ctx, *req, ctxValues.Now)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
case invite.ErrInviteExpired:
|
||||
@ -699,13 +701,13 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// Load the user without any claims applied.
|
||||
usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, hash.UserID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, auth.Claims{}, hash.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Authenticated the user. Probably should use the default session TTL from UserLogin.
|
||||
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, user_auth.AuthenticateRequest{
|
||||
token, err := h.AuthRepo.Authenticate(ctx, user_auth.AuthenticateRequest{
|
||||
Email: usr.Email,
|
||||
Password: req.Password,
|
||||
AccountID: hash.AccountID,
|
||||
@ -720,7 +722,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// Add the token to the users session.
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -729,9 +731,9 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
|
||||
return true, web.Redirect(ctx, w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
usrAcc, err := invite.AcceptInvite(ctx, h.MasterDB, invite.AcceptInviteRequest{
|
||||
usrAcc, err := h.InviteRepo.AcceptInvite(ctx, invite.AcceptInviteRequest{
|
||||
InviteHash: inviteHash,
|
||||
}, h.SecretKey, ctxValues.Now)
|
||||
}, ctxValues.Now)
|
||||
if err != nil {
|
||||
|
||||
switch errors.Cause(err) {
|
||||
@ -776,7 +778,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// Read user by ID with no claims.
|
||||
usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, usrAcc.UserID)
|
||||
usr, err := h.UserRepo.ReadByID(ctx, auth.Claims{}, usrAcc.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -799,7 +801,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
|
||||
return nil
|
||||
}
|
||||
|
||||
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
|
||||
data["timezones"], err = h.GeoRepo.ListTimezones(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -6,6 +6,13 @@ import (
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/project"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
|
||||
"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"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
@ -32,8 +39,9 @@ import (
|
||||
template_renderer "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/template-renderer"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/project_route"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
@ -51,7 +59,6 @@ import (
|
||||
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
// build is the git version of this program. It is set using build flags in the makefile.
|
||||
@ -66,10 +73,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,27 +93,29 @@ func main() {
|
||||
DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"`
|
||||
}
|
||||
Service struct {
|
||||
Name string `default:"web-app" envconfig:"NAME"`
|
||||
Project string `default:"" envconfig:"PROJECT"`
|
||||
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://example.saasstartupkit.com"`
|
||||
HostNames []string `envconfig:"HOST_NAMES" example:"www.example.saasstartupkit.com"`
|
||||
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
|
||||
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
|
||||
SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"`
|
||||
StaticFiles struct {
|
||||
Name string `default:"web-app" envconfig:"SERVICE_NAME"`
|
||||
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://example.saasstartupkit.com"`
|
||||
HostNames []string `envconfig:"HOST_NAMES" example:"www.example.saasstartupkit.com"`
|
||||
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
|
||||
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
|
||||
StaticFiles struct {
|
||||
Dir string `default:"./static" envconfig:"STATIC_DIR"`
|
||||
S3Enabled bool `envconfig:"S3_ENABLED"`
|
||||
S3Prefix string `default:"public/web_app/static" envconfig:"S3_PREFIX"`
|
||||
CloudFrontEnabled bool `envconfig:"CLOUDFRONT_ENABLED"`
|
||||
ImgResizeEnabled bool `envconfig:"IMG_RESIZE_ENABLED"`
|
||||
}
|
||||
WebApiBaseUrl string `default:"http://127.0.0.1:3001" envconfig:"WEB_API_BASE_URL" example:"http://api.example.saasstartupkit.com"`
|
||||
SessionKey string `default:"" envconfig:"SESSION_KEY"`
|
||||
SessionName string `default:"" envconfig:"SESSION_NAME"`
|
||||
EmailSender string `default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER"`
|
||||
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"`
|
||||
WebApiBaseUrl string `default:"http://127.0.0.1:3001" envconfig:"WEB_API_BASE_URL" example:"http://api.example.saasstartupkit.com"`
|
||||
}
|
||||
Redis struct {
|
||||
Host string `default:":6379" envconfig:"HOST"`
|
||||
DB int `default:"1" envconfig:"DB"`
|
||||
@ -145,12 +153,6 @@ func main() {
|
||||
UseAwsSecretManager bool `default:"false" envconfig:"USE_AWS_SECRET_MANAGER"`
|
||||
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
|
||||
}
|
||||
STMP struct {
|
||||
Host string `default:"localhost" envconfig:"HOST"`
|
||||
Port int `default:"25" envconfig:"PORT"`
|
||||
User string `default:"" envconfig:"USER"`
|
||||
Pass string `default:"" envconfig:"PASS" json:"-"` // don't print
|
||||
}
|
||||
BuildInfo struct {
|
||||
CiCommitRefName string `envconfig:"CI_COMMIT_REF_NAME"`
|
||||
CiCommitShortSha string `envconfig:"CI_COMMIT_SHORT_SHA"`
|
||||
@ -202,10 +204,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...)
|
||||
}
|
||||
@ -293,6 +295,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.
|
||||
@ -367,7 +400,8 @@ func main() {
|
||||
// Notify Email
|
||||
var notifyEmail notify.Email
|
||||
if awsSession != nil {
|
||||
notifyEmail, err = notify.NewEmailAws(awsSession, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender)
|
||||
// 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)
|
||||
}
|
||||
@ -384,15 +418,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
d := gomail.Dialer{
|
||||
Host: cfg.STMP.Host,
|
||||
Port: cfg.STMP.Port,
|
||||
Username: cfg.STMP.User,
|
||||
Password: cfg.STMP.Pass}
|
||||
notifyEmail, err = notify.NewEmailSmtp(d, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Notify Email : %+v", err)
|
||||
}
|
||||
notifyEmail = notify.NewEmailDisabled()
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@ -409,12 +435,46 @@ func main() {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Load middlewares that need to be configured specific for the service.
|
||||
// Init repositories and AppContext
|
||||
|
||||
var serviceMiddlewares = []web.Middleware{
|
||||
mid.Translator(webcontext.UniversalTranslator()),
|
||||
projectRoute, err := project_route.New(cfg.Project.WebApiBaseUrl, cfg.Service.BaseUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("main : project routes : %+v", cfg.Service.BaseUrl, err)
|
||||
}
|
||||
|
||||
usrRepo := user.NewRepository(masterDb, projectRoute.UserResetPassword, notifyEmail, cfg.Project.SharedSecretKey)
|
||||
usrAccRepo := user_account.NewRepository(masterDb)
|
||||
accRepo := account.NewRepository(masterDb)
|
||||
geoRepo := geonames.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,
|
||||
TemplateDir: cfg.Service.TemplateDir,
|
||||
StaticDir: cfg.Service.StaticFiles.Dir,
|
||||
ProjectRoute: projectRoute,
|
||||
UserRepo: usrRepo,
|
||||
UserAccountRepo: usrAccRepo,
|
||||
AccountRepo: accRepo,
|
||||
AccountPrefRepo: accPrefRepo,
|
||||
AuthRepo: authRepo,
|
||||
GeoRepo: geoRepo,
|
||||
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{
|
||||
@ -430,52 +490,23 @@ 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()))
|
||||
|
||||
// Generate the new session store and append it to the global list of middlewares.
|
||||
|
||||
// Init session store
|
||||
if cfg.Service.SessionName == "" {
|
||||
cfg.Service.SessionName = fmt.Sprintf("%s-session", cfg.Service.Name)
|
||||
}
|
||||
|
||||
// Set the session key if not provided in the config.
|
||||
if cfg.Service.SessionKey == "" {
|
||||
|
||||
// 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, "session")
|
||||
|
||||
// If AWS is enabled, check the Secrets Manager for the session key.
|
||||
if awsSession != nil {
|
||||
cfg.Service.SessionKey, 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.Service.SessionKey == "" {
|
||||
cfg.Service.SessionKey = string(securecookie.GenerateRandomKey(32))
|
||||
|
||||
if awsSession != nil {
|
||||
err = devops.SecretManagerPutString(awsSession, secretID, cfg.Service.SessionKey)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Session : %+v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the new session store and append it to the global list of middlewares.
|
||||
sessionStore := sessions.NewCookieStore([]byte(cfg.Service.SessionKey))
|
||||
serviceMiddlewares = append(serviceMiddlewares, mid.Session(sessionStore, cfg.Service.SessionName))
|
||||
sessionStore := sessions.NewCookieStore([]byte(cfg.Project.SharedSecretKey))
|
||||
appCtx.PostAppMiddleware = append(appCtx.PostAppMiddleware, mid.Session(sessionStore, cfg.Service.SessionName))
|
||||
|
||||
// =========================================================================
|
||||
// URL Formatter
|
||||
projectRoutes, err := project_routes.New(cfg.Service.WebApiBaseUrl, cfg.Service.BaseUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("main : project routes : %+v", cfg.Service.BaseUrl, err)
|
||||
}
|
||||
|
||||
// s3UrlFormatter is a help function used by to convert an s3 key to
|
||||
// a publicly available image URL.
|
||||
@ -495,7 +526,7 @@ func main() {
|
||||
return s3UrlFormatter(p)
|
||||
}
|
||||
} else {
|
||||
staticS3UrlFormatter = projectRoutes.WebAppUrl
|
||||
staticS3UrlFormatter = projectRoute.WebAppUrl
|
||||
}
|
||||
|
||||
// staticUrlFormatter is a help function used by template functions defined below.
|
||||
@ -698,7 +729,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
usr, err := user.ReadByID(ctx, auth.Claims{}, masterDb, claims.Subject)
|
||||
usr, err := usrRepo.ReadByID(ctx, auth.Claims{}, claims.Subject)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -733,7 +764,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
acc, err := account.ReadByID(ctx, auth.Claims{}, masterDb, claims.Audience)
|
||||
acc, err := accRepo.ReadByID(ctx, auth.Claims{}, claims.Audience)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -874,7 +905,7 @@ func main() {
|
||||
enableHotReload := cfg.Env == "dev"
|
||||
|
||||
// Template Renderer used to generate HTML response for web experience.
|
||||
renderer, err := template_renderer.NewTemplateRenderer(cfg.Service.TemplateDir, enableHotReload, gvd, t, eh)
|
||||
appCtx.Renderer, err = template_renderer.NewTemplateRenderer(cfg.Service.TemplateDir, enableHotReload, gvd, t, eh)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Marshalling Config to JSON : %+v", err)
|
||||
}
|
||||
@ -926,7 +957,7 @@ func main() {
|
||||
if cfg.HTTP.Host != "" {
|
||||
api := http.Server{
|
||||
Addr: cfg.HTTP.Host,
|
||||
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, cfg.Service.SessionKey, notifyEmail, renderer, serviceMiddlewares...),
|
||||
Handler: handlers.APP(shutdown, appCtx),
|
||||
ReadTimeout: cfg.HTTP.ReadTimeout,
|
||||
WriteTimeout: cfg.HTTP.WriteTimeout,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
@ -943,7 +974,7 @@ func main() {
|
||||
if cfg.HTTPS.Host != "" {
|
||||
api := http.Server{
|
||||
Addr: cfg.HTTPS.Host,
|
||||
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, cfg.Service.SessionKey, notifyEmail, renderer, serviceMiddlewares...),
|
||||
Handler: handlers.APP(shutdown, appCtx),
|
||||
ReadTimeout: cfg.HTTPS.ReadTimeout,
|
||||
WriteTimeout: cfg.HTTPS.WriteTimeout,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
|
Reference in New Issue
Block a user