1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-07-15 01:34:32 +02:00

Completed web-api and web-app updates

This commit is contained in:
Lee Brown
2019-08-14 17:59:47 -08:00
parent 04e73c8f4e
commit 102ca82125
13 changed files with 312 additions and 234 deletions

View File

@ -34,12 +34,13 @@
{"name": "ECS_SERVICE", "value": "{ECS_SERVICE}"}, {"name": "ECS_SERVICE", "value": "{ECS_SERVICE}"},
{"name": "WEB_API_HTTP_HOST", "value": "{HTTP_HOST}"}, {"name": "WEB_API_HTTP_HOST", "value": "{HTTP_HOST}"},
{"name": "WEB_API_HTTPS_HOST", "value": "{HTTPS_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_BASE_URL", "value": "{APP_BASE_URL}"},
{"name": "WEB_API_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"}, {"name": "WEB_API_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"},
{"name": "WEB_API_SERVICE_ENABLE_HTTPS", "value": "{HTTPS_ENABLED}"}, {"name": "WEB_API_SERVICE_ENABLE_HTTPS", "value": "{HTTPS_ENABLED}"},
{"name": "WEB_API_SERVICE_EMAIL_SENDER", "value": "{EMAIL_SENDER}"}, {"name": "WEB_API_PROJECT_PROJECT_NAME", "value": "{APP_PROJECT}"},
{"name": "WEB_API_SERVICE_WEB_APP_BASE_URL", "value": "{WEB_APP_BASE_URL}"}, {"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_REDIS_HOST", "value": "{CACHE_HOST}"},
{"name": "WEB_API_DB_HOST", "value": "{DB_HOST}"}, {"name": "WEB_API_DB_HOST", "value": "{DB_HOST}"},
{"name": "WEB_API_DB_USER", "value": "{DB_USER}"}, {"name": "WEB_API_DB_USER", "value": "{DB_USER}"},

View File

@ -97,7 +97,7 @@ func main() {
DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"` DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"`
} }
Service struct { Service struct {
Name string `default:"web-api" envconfig:"SERVICE"` Name string `default:"web-api" envconfig:"SERVICE_NAME"`
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://api.example.saasstartupkit.com"` BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://api.example.saasstartupkit.com"`
HostNames []string `envconfig:"HOST_NAMES" example:"alternative-subdomain.example.saasstartupkit.com"` HostNames []string `envconfig:"HOST_NAMES" example:"alternative-subdomain.example.saasstartupkit.com"`
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"` EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
@ -106,7 +106,7 @@ func main() {
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
} }
Project struct { Project struct {
Name string `default:"" envconfig:"PROJECT"` Name string `default:"" envconfig:"PROJECT_NAME"`
SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"` SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"`
SharedSecretKey string `default:"" envconfig:"SHARED_SECRET_KEY"` SharedSecretKey string `default:"" envconfig:"SHARED_SECRET_KEY"`
EmailSender string `default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER"` EmailSender string `default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER"`
@ -203,7 +203,7 @@ func main() {
if cfg.Project.Name != "" { if cfg.Project.Name != "" {
pts = append(pts, 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...) cfg.Aws.SecretsManagerConfigPrefix = filepath.Join(pts...)
} }
@ -299,7 +299,7 @@ func main() {
// AWS secrets manager ID for storing the session key. This is optional and only will be used // AWS secrets manager ID for storing the session key. This is optional and only will be used
// if a valid AWS session is provided. // if a valid AWS session is provided.
secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "sharedSecretKey") secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "SharedSecretKey")
// If AWS is enabled, check the Secrets Manager for the session key. // If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil { if awsSession != nil {

View File

@ -34,7 +34,7 @@
{"name": "ECS_SERVICE", "value": "{ECS_SERVICE}"}, {"name": "ECS_SERVICE", "value": "{ECS_SERVICE}"},
{"name": "WEB_APP_HTTP_HOST", "value": "{HTTP_HOST}"}, {"name": "WEB_APP_HTTP_HOST", "value": "{HTTP_HOST}"},
{"name": "WEB_APP_HTTPS_HOST", "value": "{HTTPS_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_BASE_URL", "value": "{APP_BASE_URL}"},
{"name": "WEB_APP_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"}, {"name": "WEB_APP_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"},
{"name": "WEB_APP_SERVICE_ENABLE_HTTPS", "value": "{HTTPS_ENABLED}"}, {"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_S3_PREFIX", "value": "{STATIC_FILES_S3_PREFIX}"},
{"name": "WEB_APP_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", "value": "{STATIC_FILES_CLOUDFRONT_ENABLED}"}, {"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_STATICFILES_IMG_RESIZE_ENABLED", "value": "{STATIC_FILES_IMG_RESIZE_ENABLED}"},
{"name": "WEB_APP_SERVICE_EMAIL_SENDER", "value": "{EMAIL_SENDER}"}, {"name": "WEB_APP_PROJECT_PROJECT_NAME", "value": "{APP_PROJECT}"},
{"name": "WEB_APP_SERVICE_WEB_API_BASE_URL", "value": "{WEB_API_BASE_URL}"}, {"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_REDIS_HOST", "value": "{CACHE_HOST}"},
{"name": "WEB_APP_DB_HOST", "value": "{DB_HOST}"}, {"name": "WEB_APP_DB_HOST", "value": "{DB_HOST}"},
{"name": "WEB_APP_DB_USER", "value": "{DB_USER}"}, {"name": "WEB_APP_DB_USER", "value": "{DB_USER}"},

View File

@ -12,6 +12,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "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/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/user_auth"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -19,9 +20,12 @@ import (
// Account represents the Account API method handler set. // Account represents the Account API method handler set.
type Account struct { type Account struct {
AccountRepo *account.Repository
AccountPrefRepo *account_preference.Repository
AuthRepo *user_auth.Repository
Authenticator *auth.Authenticator
MasterDB *sqlx.DB MasterDB *sqlx.DB
Renderer web.Renderer Renderer web.Renderer
Authenticator *auth.Authenticator
} }
// View handles displaying the current account profile. // 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 return err
} }
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience) acc, err := h.AccountRepo.ReadByID(ctx, claims, claims.Audience)
if err != nil { if err != nil {
return err return err
} }
@ -77,7 +81,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
return false, err 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, AccountID: claims.Audience,
}) })
if err != nil { if err != nil {
@ -115,7 +119,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
} }
req.ID = claims.Audience 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -135,7 +139,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
} }
if preferenceDatetimeFormat != req.PreferenceDatetimeFormat { 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, AccountID: claims.Audience,
Name: account_preference.AccountPreference_Datetime_Format, Name: account_preference.AccountPreference_Datetime_Format,
Value: req.PreferenceDatetimeFormat, Value: req.PreferenceDatetimeFormat,
@ -156,7 +160,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
} }
if preferenceDateFormat != req.PreferenceDateFormat { 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, AccountID: claims.Audience,
Name: account_preference.AccountPreference_Date_Format, Name: account_preference.AccountPreference_Date_Format,
Value: req.PreferenceDateFormat, Value: req.PreferenceDateFormat,
@ -177,7 +181,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
} }
if preferenceTimeFormat != req.PreferenceTimeFormat { 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, AccountID: claims.Audience,
Name: account_preference.AccountPreference_Time_Format, Name: account_preference.AccountPreference_Time_Format,
Value: req.PreferenceTimeFormat, 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) 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 { if err != nil {
return false, err return false, err
} }

View File

@ -13,14 +13,13 @@ import (
"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/project"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
) )
// Projects represents the Projects API method handler set. // Projects represents the Projects API method handler set.
type Projects struct { type Projects struct {
MasterDB *sqlx.DB ProjectRepo *project.Repository
Redis *redis.Client Redis *redis.Client
Renderer web.Renderer Renderer web.Renderer
} }
@ -110,7 +109,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) { 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 = ?", Where: "account_id = ?",
Args: []interface{}{claims.Audience}, Args: []interface{}{claims.Audience},
Order: strings.Split(sorting, ","), Order: strings.Split(sorting, ","),
@ -186,7 +185,7 @@ func (h *Projects) Create(ctx context.Context, w http.ResponseWriter, r *http.Re
} }
req.AccountID = claims.Audience 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -251,7 +250,7 @@ func (h *Projects) View(ctx context.Context, w http.ResponseWriter, r *http.Requ
switch r.PostForm.Get("action") { switch r.PostForm.Get("action") {
case "archive": case "archive":
err = project.Archive(ctx, claims, h.MasterDB, project.ProjectArchiveRequest{ err = h.ProjectRepo.Archive(ctx, claims, project.ProjectArchiveRequest{
ID: projectID, ID: projectID,
}, ctxValues.Now) }, ctxValues.Now)
if err != nil { if err != nil {
@ -276,7 +275,7 @@ func (h *Projects) View(ctx context.Context, w http.ResponseWriter, r *http.Requ
return nil return nil
} }
prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID) prj, err := h.ProjectRepo.ReadByID(ctx, claims, projectID)
if err != nil { if err != nil {
return err return err
} }
@ -320,7 +319,7 @@ func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Re
} }
req.ID = projectID 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -351,7 +350,7 @@ func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Re
return nil return nil
} }
prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID) prj, err := h.ProjectRepo.ReadByID(ctx, claims, projectID)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,9 +8,8 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "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"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "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/ikeikeikeike/go-sitemap-generator/v2/stm"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sethgrid/pester" "github.com/sethgrid/pester"
"io/ioutil" "io/ioutil"
@ -19,10 +18,9 @@ import (
// Root represents the Root API method handler set. // Root represents the Root API method handler set.
type Root struct { type Root struct {
MasterDB *sqlx.DB
Renderer web.Renderer Renderer web.Renderer
Sitemap *stm.Sitemap Sitemap *stm.Sitemap
ProjectRoutes project_routes.ProjectRoutes ProjectRoute project_route.ProjectRoute
} }
// Index determines if the user has authentication and loads the associated page. // 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" tmpName = "site-api.gohtml"
// http://127.0.0.1:3001/docs/doc.json // 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. // Load the json file from the API service.
res, err := pester.Get(swaggerJsonUrl) 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) return errors.WithStack(err)
} }
data["urlApiBaseUri"] = h.ProjectRoutes.WebApiUrl(doc.BasePath) data["urlApiBaseUri"] = h.ProjectRoute.WebApiUrl(doc.BasePath)
data["urlApiDocs"] = h.ProjectRoutes.ApiDocs() data["urlApiDocs"] = h.ProjectRoute.ApiDocs()
case "/pricing": case "/pricing":
tmpName = "site-pricing.gohtml" 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) 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) txt := fmt.Sprintf("User-agent: *\nDisallow: /ping\nDisallow: /status\nDisallow: /debug/\nSitemap: %s", sitemapUrl)
return web.RespondText(ctx, w, txt, http.StatusOK) return web.RespondText(ctx, w, txt, http.StatusOK)

View File

@ -9,13 +9,20 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"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/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "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"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "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"
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/ikeikeikeike/go-sitemap-generator/v2/stm"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
@ -27,30 +34,58 @@ const (
TmplContentErrorGeneric = "error-generic.gohtml" TmplContentErrorGeneric = "error-generic.gohtml"
) )
type AppContext struct {
Log *log.Logger
Env webcontext.Env
MasterDB *sqlx.DB
Redis *redis.Client
UserRepo *user.Repository
UserAccountRepo *user_account.Repository
AccountRepo *account.Repository
AccountPrefRepo *account_preference.Repository
AuthRepo *user_auth.Repository
SignupRepo *signup.Repository
InviteRepo *invite.Repository
ProjectRepo *project.Repository
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. // 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. // Include the pre middlewares first.
middlewares := []web.Middleware{ middlewares := appCtx.PreAppMiddleware
mid.Trace(), mid.Logger(log), mid.Errors(log, renderer), mid.Metrics(), mid.Panics(),
}
// Append any global middlewares if they were included. // Define app middlewares applied to all requests.
if len(globalMids) > 0 { middlewares = append(middlewares,
middlewares = append(middlewares, globalMids...) 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. // 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. // Build a sitemap.
sm := stm.NewSitemap(1) sm := stm.NewSitemap(1)
sm.SetVerbose(false) sm.SetVerbose(false)
sm.SetDefaultHost(projectRoutes.WebAppUrl("")) sm.SetDefaultHost(appCtx.ProjectRoute.WebAppUrl(""))
sm.Create() sm.Create()
smLocAddModified := func(loc stm.URL, filename string) { 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) file, err := os.Stat(contentPath)
if err != nil { if err != nil {
@ -64,48 +99,48 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
// Register project management pages. // Register project management pages.
p := Projects{ p := Projects{
MasterDB: masterDB, ProjectRepo: appCtx.ProjectRepo,
Redis: redis, Redis: appCtx.Redis,
Renderer: renderer, Renderer: appCtx.Renderer,
} }
app.Handle("POST", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) 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(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(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(authenticator), mid.HasAuth()) app.Handle("GET", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
app.Handle("POST", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) app.Handle("POST", "/projects/create", p.Create, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/projects/create", p.Create, mid.AuthenticateSessionRequired(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(authenticator), mid.HasAuth()) app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
// Register user management pages. // Register user management pages.
us := Users{ us := Users{
MasterDB: masterDB, UserRepo: appCtx.UserRepo,
Redis: redis, UserAccountRepo: appCtx.UserAccountRepo,
Renderer: renderer, AuthRepo: appCtx.AuthRepo,
Authenticator: authenticator, InviteRepo: appCtx.InviteRepo,
ProjectRoutes: projectRoutes, MasterDB: appCtx.MasterDB,
NotifyEmail: notifyEmail, Redis: appCtx.Redis,
SecretKey: secretKey, Renderer: appCtx.Renderer,
} }
app.Handle("POST", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) 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(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(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(authenticator), mid.HasAuth()) app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
app.Handle("POST", "/users/invite/:hash", us.InviteAccept) app.Handle("POST", "/users/invite/:hash", us.InviteAccept)
app.Handle("GET", "/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("POST", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(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(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(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(authenticator), mid.HasAuth()) app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
// Register user management and authentication endpoints. // Register user management and authentication endpoints.
u := User{ u := User{
MasterDB: masterDB, UserRepo: appCtx.UserRepo,
Renderer: renderer, UserAccountRepo: appCtx.UserAccountRepo,
Authenticator: authenticator, AccountRepo: appCtx.AccountRepo,
ProjectRoutes: projectRoutes, AuthRepo: appCtx.AuthRepo,
NotifyEmail: notifyEmail, MasterDB: appCtx.MasterDB,
SecretKey: secretKey, Renderer: appCtx.Renderer,
} }
app.Handle("POST", "/user/login", u.Login) app.Handle("POST", "/user/login", u.Login)
app.Handle("GET", "/user/login", u.Login) app.Handle("GET", "/user/login", u.Login)
@ -114,35 +149,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("GET", "/user/reset-password/:hash", u.ResetConfirm)
app.Handle("POST", "/user/reset-password", u.ResetPassword) app.Handle("POST", "/user/reset-password", u.ResetPassword)
app.Handle("GET", "/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("POST", "/user/update", u.Update, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
app.Handle("GET", "/user/update", u.Update, mid.AuthenticateSessionRequired(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(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(authenticator), mid.HasRole(auth.RoleAdmin)) 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(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(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(authenticator), mid.HasAuth()) 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(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(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(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(authenticator), mid.HasAuth()) app.Handle("POST", "/user", u.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
app.Handle("GET", "/user", u.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth()) app.Handle("GET", "/user", u.View, mid.AuthenticateSessionRequired(appCtx.Authenticator), mid.HasAuth())
// Register account management endpoints. // Register account management endpoints.
acc := Account{ acc := Account{
MasterDB: masterDB, AccountRepo: appCtx.AccountRepo,
Renderer: renderer, AccountPrefRepo: appCtx.AccountPrefRepo,
Authenticator: authenticator, AuthRepo: appCtx.AuthRepo,
Authenticator: appCtx.Authenticator,
MasterDB: appCtx.MasterDB,
Renderer: appCtx.Renderer,
} }
app.Handle("POST", "/account/update", acc.Update, 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(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(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(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{ s := Signup{
MasterDB: masterDB, SignupRepo: appCtx.SignupRepo,
Renderer: renderer, AuthRepo: appCtx.AuthRepo,
Authenticator: authenticator, MasterDB: appCtx.MasterDB,
Renderer: appCtx.Renderer,
} }
// This route is not authenticated // This route is not authenticated
app.Handle("POST", "/signup", s.Step1) app.Handle("POST", "/signup", s.Step1)
@ -150,16 +189,16 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
// Register example endpoints. // Register example endpoints.
ex := Examples{ ex := Examples{
Renderer: renderer, Renderer: appCtx.Renderer,
} }
app.Handle("POST", "/examples/flash-messages", ex.FlashMessages, 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(authenticator)) app.Handle("GET", "/examples/flash-messages", ex.FlashMessages, mid.AuthenticateSessionOptional(appCtx.Authenticator))
app.Handle("GET", "/examples/images", ex.Images, mid.AuthenticateSessionOptional(authenticator)) app.Handle("GET", "/examples/images", ex.Images, mid.AuthenticateSessionOptional(appCtx.Authenticator))
// Register geo // Register geo
g := Geo{ g := Geo{
MasterDB: masterDB, MasterDB: appCtx.MasterDB,
Redis: redis, Redis: appCtx.Redis,
} }
app.Handle("GET", "/geo/regions/autocomplete", g.RegionsAutocomplete) app.Handle("GET", "/geo/regions/autocomplete", g.RegionsAutocomplete)
app.Handle("GET", "/geo/postal_codes/autocomplete", g.PostalCodesAutocomplete) app.Handle("GET", "/geo/postal_codes/autocomplete", g.PostalCodesAutocomplete)
@ -168,9 +207,8 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
// Register root // Register root
r := Root{ r := Root{
MasterDB: masterDB, Renderer: appCtx.Renderer,
Renderer: renderer, ProjectRoute: appCtx.ProjectRoute,
ProjectRoutes: projectRoutes,
Sitemap: sm, Sitemap: sm,
} }
app.Handle("GET", "/api", r.SitePage) app.Handle("GET", "/api", r.SitePage)
@ -178,7 +216,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
app.Handle("GET", "/support", r.SitePage) app.Handle("GET", "/support", r.SitePage)
app.Handle("GET", "/legal/privacy", r.SitePage) app.Handle("GET", "/legal/privacy", r.SitePage)
app.Handle("GET", "/legal/terms", 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", "/index.html", r.IndexHtml)
app.Handle("GET", "/robots.txt", r.RobotTxt) app.Handle("GET", "/robots.txt", r.RobotTxt)
app.Handle("GET", "/sitemap.xml", r.SitemapXml) app.Handle("GET", "/sitemap.xml", r.SitemapXml)
@ -193,14 +231,14 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
// Register health check endpoint. This route is not authenticated. // Register health check endpoint. This route is not authenticated.
check := Check{ check := Check{
MasterDB: masterDB, MasterDB: appCtx.MasterDB,
Redis: redis, Redis: appCtx.Redis,
} }
app.Handle("GET", "/v1/health", check.Health) app.Handle("GET", "/v1/health", check.Health)
// Handle static files/pages. Render a custom 404 page when file not found. // 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 { 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 err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
rmsg := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI) rmsg := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
@ -209,7 +247,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
err = weberror.NewError(ctx, err, http.StatusInternalServerError) 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 return nil

View File

@ -20,9 +20,10 @@ import (
// Signup represents the Signup API method handler set. // Signup represents the Signup API method handler set.
type Signup struct { type Signup struct {
SignupRepo *signup.Repository
AuthRepo *user_auth.Repository
MasterDB *sqlx.DB MasterDB *sqlx.DB
Renderer web.Renderer Renderer web.Renderer
Authenticator *auth.Authenticator
} }
// Step1 handles collecting the first detailed needed to create a new account. // Step1 handles collecting the first detailed needed to create a new account.
@ -52,7 +53,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
// Execute the account / user signup. // 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
case account.ErrForbidden: case account.ErrForbidden:
@ -68,7 +69,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
// Authenticated the new user. // 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, Email: req.User.Email,
Password: req.User.Password, Password: req.User.Password,
}, time.Hour, ctxValues.Now) }, time.Hour, ctxValues.Now)
@ -77,7 +78,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, h.MasterDB, w, r, token) err = handleSessionToken(ctx, w, r, token)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -11,11 +11,9 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/account" "geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/geonames" "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/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"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "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"
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"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account" "geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth" "geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
@ -27,12 +25,12 @@ import (
// User represents the User API method handler set. // User represents the User API method handler set.
type User struct { type User struct {
UserRepo *user.Repository
AuthRepo *user_auth.Repository
UserAccountRepo *user_account.Repository
AccountRepo *account.Repository
MasterDB *sqlx.DB MasterDB *sqlx.DB
Renderer web.Renderer Renderer web.Renderer
Authenticator *auth.Authenticator
ProjectRoutes project_routes.ProjectRoutes
NotifyEmail notify.Email
SecretKey string
} }
func urlUserVirtualLogin(userID string) string { func urlUserVirtualLogin(userID string) string {
@ -75,7 +73,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
} }
// Authenticated the user. // 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, Email: req.Email,
Password: req.Password, Password: req.Password,
}, sessionTTL, ctxValues.Now) }, sessionTTL, ctxValues.Now)
@ -97,7 +95,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, h.MasterDB, w, r, token) err = handleSessionToken(ctx, w, r, token)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -173,7 +171,7 @@ func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http
return err 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -238,7 +236,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
// Append the query param value to the request. // Append the query param value to the request.
req.ResetHash = resetHash 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
case user.ErrResetExpired: case user.ErrResetExpired:
@ -257,7 +255,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. // 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, Email: u.Email,
Password: req.Password, Password: req.Password,
}, time.Hour, ctxValues.Now) }, time.Hour, ctxValues.Now)
@ -271,7 +269,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, h.MasterDB, w, r, token) err = handleSessionToken(ctx, w, r, token)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -280,7 +278,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
return true, web.Redirect(ctx, w, r, "/", http.StatusFound) return true, web.Redirect(ctx, w, r, "/", http.StatusFound)
} }
_, err = user.ParseResetHash(ctx, h.SecretKey, resetHash, ctxValues.Now) _, err = h.UserRepo.ParseResetHash(ctx, resetHash, ctxValues.Now)
if err != nil { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
case user.ErrResetExpired: case user.ErrResetExpired:
@ -328,14 +326,14 @@ func (h *User) View(ctx context.Context, w http.ResponseWriter, r *http.Request,
return err return err
} }
usr, err := user.ReadByID(ctx, claims, h.MasterDB, claims.Subject) usr, err := h.UserRepo.ReadByID(ctx, claims, claims.Subject)
if err != nil { if err != nil {
return err return err
} }
data["user"] = usr.Response(ctx) 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 { if err != nil {
return err return err
} }
@ -388,7 +386,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
} }
req.ID = claims.Subject 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -409,7 +407,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
} }
pwdReq.ID = claims.Subject 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -441,7 +439,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
return nil return nil
} }
usr, err := user.ReadByID(ctx, claims, h.MasterDB, claims.Subject) usr, err := h.UserRepo.ReadByID(ctx, claims, claims.Subject)
if err != nil { if err != nil {
return err return err
} }
@ -484,7 +482,7 @@ func (h *User) Account(ctx context.Context, w http.ResponseWriter, r *http.Reque
return err return err
} }
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience) acc, err := h.AccountRepo.ReadByID(ctx, claims, claims.Audience)
if err != nil { if err != nil {
return err return err
} }
@ -551,7 +549,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
} }
// Perform the account switch. // 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 err != nil {
if verr, ok := weberror.NewValidationError(ctx, err); ok { if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error) data["validationErrors"] = verr.(*weberror.Error)
@ -565,7 +563,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
sess = webcontext.SessionUpdateAccessToken(sess, tkn.AccessToken) sess = webcontext.SessionUpdateAccessToken(sess, tkn.AccessToken)
// Read the account for a flash message. // 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 { if err != nil {
return false, err return false, err
} }
@ -588,7 +586,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
return nil 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 = ?", Where: "account_id = ?",
Args: []interface{}{claims.Audience}, Args: []interface{}{claims.Audience},
}) })
@ -612,7 +610,7 @@ func (h *User) VirtualLogin(ctx context.Context, w http.ResponseWriter, r *http.
userPhs = append(userPhs, "?") 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)", Where: fmt.Sprintf("id IN (%s)",
strings.Join(userPhs, ", ")), strings.Join(userPhs, ", ")),
Args: userIDs, Args: userIDs,
@ -657,7 +655,7 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
expires = time.Hour 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 { if err != nil {
return err return err
} }
@ -667,11 +665,11 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
// Display a success message to verify the user has switched contexts. // Display a success message to verify the user has switched contexts.
if claims.Subject != tkn.UserID && claims.Audience != tkn.AccountID { 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 { if err != nil {
return err return err
} }
acc, err := account.ReadByID(ctx, claims, h.MasterDB, tkn.AccountID) acc, err := h.AccountRepo.ReadByID(ctx, claims, tkn.AccountID)
if err != nil { if err != nil {
return err return err
} }
@ -680,7 +678,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.", fmt.Sprintf("You are now virtually logged back into account %s user %s.",
acc.Response(ctx).Name, usr.Response(ctx).Name)) acc.Response(ctx).Name, usr.Response(ctx).Name))
} else if claims.Audience != tkn.AccountID { } 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 { if err != nil {
return err return err
} }
@ -689,7 +687,7 @@ func (h *User) VirtualLogout(ctx context.Context, w http.ResponseWriter, r *http
fmt.Sprintf("You are now virtually logged back into account %s.", fmt.Sprintf("You are now virtually logged back into account %s.",
acc.Response(ctx).Name)) acc.Response(ctx).Name))
} else { } else {
usr, err := user.ReadByID(ctx, claims, h.MasterDB, tkn.UserID) usr, err := h.UserRepo.ReadByID(ctx, claims, tkn.UserID)
if err != nil { if err != nil {
return err return err
} }
@ -757,7 +755,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
} }
// Perform the account switch. // 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 err != nil {
if verr, ok := weberror.NewValidationError(ctx, err); ok { if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error) data["validationErrors"] = verr.(*weberror.Error)
@ -771,7 +769,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
sess = webcontext.SessionUpdateAccessToken(sess, tkn.AccessToken) sess = webcontext.SessionUpdateAccessToken(sess, tkn.AccessToken)
// Read the account for a flash message. // 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 { if err != nil {
return false, err return false, err
} }
@ -794,7 +792,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
return nil return nil
} }
accounts, err := account.Find(ctx, claims, h.MasterDB, account.AccountFindRequest{ accounts, err := h.AccountRepo.Find(ctx, claims, account.AccountFindRequest{
Order: []string{"name"}, Order: []string{"name"},
}) })
if err != nil { if err != nil {
@ -816,7 +814,7 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
} }
// handleSessionToken persists the access token to the session for request authentication. // 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 == "" { if token.AccessToken == "" {
return errors.New("accessToken is required.") return errors.New("accessToken is required.")
} }

View File

@ -3,14 +3,16 @@ package handlers
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"strings"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/geonames" "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/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable" "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"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "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"
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"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account" "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_account/invite"
@ -20,20 +22,17 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
"net/http"
"strings"
"time"
) )
// Users represents the Users API method handler set. // Users represents the Users API method handler set.
type Users struct { type Users struct {
UserRepo *user.Repository
UserAccountRepo *user_account.Repository
AuthRepo *user_auth.Repository
InviteRepo *invite.Repository
MasterDB *sqlx.DB MasterDB *sqlx.DB
Redis *redis.Client Redis *redis.Client
Renderer web.Renderer Renderer web.Renderer
Authenticator *auth.Authenticator
ProjectRoutes project_routes.ProjectRoutes
NotifyEmail notify.Email
SecretKey string
} }
func urlUsersIndex() string { func urlUsersIndex() string {
@ -144,7 +143,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) { 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, AccountID: claims.Audience,
Order: strings.Split(sorting, ","), Order: strings.Split(sorting, ","),
}) })
@ -232,7 +231,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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -246,7 +245,7 @@ func (h *Users) Create(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
uaStatus := user_account.UserAccountStatus_Active 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, UserID: usr.ID,
AccountID: claims.Audience, AccountID: claims.Audience,
Roles: req.Roles, Roles: req.Roles,
@ -327,7 +326,7 @@ func (h *Users) View(ctx context.Context, w http.ResponseWriter, r *http.Request
switch r.PostForm.Get("action") { switch r.PostForm.Get("action") {
case "archive": case "archive":
err = user.Archive(ctx, claims, h.MasterDB, user.UserArchiveRequest{ err = h.UserRepo.Archive(ctx, claims, user.UserArchiveRequest{
ID: userID, ID: userID,
}, ctxValues.Now) }, ctxValues.Now)
if err != nil { if err != nil {
@ -352,14 +351,14 @@ func (h *Users) View(ctx context.Context, w http.ResponseWriter, r *http.Request
return nil return nil
} }
usr, err := user.ReadByID(ctx, claims, h.MasterDB, userID) usr, err := h.UserRepo.ReadByID(ctx, claims, userID)
if err != nil { if err != nil {
return err return err
} }
data["user"] = usr.Response(ctx) 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 { if err != nil {
return err return err
} }
@ -425,7 +424,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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -439,7 +438,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
if req.Roles != nil { 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, UserID: userID,
AccountID: claims.Audience, AccountID: claims.Audience,
Roles: &req.Roles, Roles: &req.Roles,
@ -465,7 +464,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
pwdReq.ID = userID 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -497,12 +496,12 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
return nil return nil
} }
usr, err := user.ReadByID(ctx, claims, h.MasterDB, userID) usr, err := h.UserRepo.ReadByID(ctx, claims, userID)
if err != nil { if err != nil {
return err 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, UserID: userID,
AccountID: claims.Audience, AccountID: claims.Audience,
}) })
@ -577,7 +576,7 @@ func (h *Users) Invite(ctx context.Context, w http.ResponseWriter, r *http.Reque
req.UserID = claims.Subject req.UserID = claims.Subject
req.AccountID = claims.Audience 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
default: default:
@ -661,7 +660,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
// Append the query param value to the request. // Append the query param value to the request.
req.InviteHash = inviteHash 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 { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
case invite.ErrInviteExpired: case invite.ErrInviteExpired:
@ -699,13 +698,13 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
} }
// Load the user without any claims applied. // 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 { if err != nil {
return false, err return false, err
} }
// Authenticated the user. Probably should use the default session TTL from UserLogin. // 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, Email: usr.Email,
Password: req.Password, Password: req.Password,
AccountID: hash.AccountID, AccountID: hash.AccountID,
@ -720,7 +719,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, h.MasterDB, w, r, token) err = handleSessionToken(ctx, w, r, token)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -729,9 +728,9 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
return true, web.Redirect(ctx, w, r, "/", http.StatusFound) 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, InviteHash: inviteHash,
}, h.SecretKey, ctxValues.Now) }, ctxValues.Now)
if err != nil { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
@ -776,7 +775,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
} }
// Read user by ID with no claims. // 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 { if err != nil {
return false, err return false, err
} }

View File

@ -6,7 +6,12 @@ import (
"encoding/json" "encoding/json"
"expvar" "expvar"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/project_route" "geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
"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" "html/template"
"log" "log"
"net" "net"
@ -33,7 +38,7 @@ import (
template_renderer "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/template-renderer" 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/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" "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" "geeks-accelerator/oss/saas-starter-kit/internal/user"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
@ -52,7 +57,6 @@ import (
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx" sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "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. // build is the git version of this program. It is set using build flags in the makefile.
@ -67,10 +71,9 @@ func main() {
// ========================================================================= // =========================================================================
// Logging // Logging
log.SetFlags(log.LstdFlags|log.Lmicroseconds|log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
log.SetPrefix(service+" : ") log.SetPrefix(service + " : ")
log := log.New(os.Stdout, log.Prefix() , log.Flags()) log := log.New(os.Stdout, log.Prefix(), log.Flags())
// ========================================================================= // =========================================================================
// Configuration // Configuration
@ -88,7 +91,7 @@ func main() {
DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"` DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"`
} }
Service struct { Service struct {
Name string `default:"web-app" envconfig:"NAME"` Name string `default:"web-app" envconfig:"SERVICE_NAME"`
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://example.saasstartupkit.com"` BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://example.saasstartupkit.com"`
HostNames []string `envconfig:"HOST_NAMES" example:"www.example.saasstartupkit.com"` HostNames []string `envconfig:"HOST_NAMES" example:"www.example.saasstartupkit.com"`
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"` EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
@ -105,7 +108,7 @@ func main() {
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
} }
Project struct { Project struct {
Name string `default:"" envconfig:"PROJECT"` Name string `default:"" envconfig:"PROJECT_NAME"`
SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"` SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"`
SharedSecretKey string `default:"" envconfig:"SHARED_SECRET_KEY"` SharedSecretKey string `default:"" envconfig:"SHARED_SECRET_KEY"`
EmailSender string `default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER"` EmailSender string `default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER"`
@ -202,7 +205,7 @@ func main() {
if cfg.Project.Name != "" { if cfg.Project.Name != "" {
pts = append(pts, 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...) cfg.Aws.SecretsManagerConfigPrefix = filepath.Join(pts...)
} }
@ -298,7 +301,7 @@ func main() {
// AWS secrets manager ID for storing the session key. This is optional and only will be used // AWS secrets manager ID for storing the session key. This is optional and only will be used
// if a valid AWS session is provided. // if a valid AWS session is provided.
secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "sharedSecretKey") secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "SharedSecretKey")
// If AWS is enabled, check the Secrets Manager for the session key. // If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil { if awsSession != nil {
@ -313,7 +316,7 @@ func main() {
cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32)) cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32))
if awsSession != nil { if awsSession != nil {
err = devops.SecretManagerPutString(awsSession, secretID, cfg.Service.SecretKey) err = devops.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey)
if err != nil { if err != nil {
log.Fatalf("main : Session : %+v", err) log.Fatalf("main : Session : %+v", err)
} }
@ -396,7 +399,7 @@ func main() {
var notifyEmail notify.Email var notifyEmail notify.Email
if awsSession != nil { if awsSession != nil {
// Send emails with AWS SES. Alternative to use SMTP with notify.NewEmailSmtp. // Send emails with AWS SES. Alternative to use SMTP with notify.NewEmailSmtp.
notifyEmail, err = notify.NewEmailAws(awsSession, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender) notifyEmail, err = notify.NewEmailAws(awsSession, cfg.Project.SharedTemplateDir, cfg.Project.EmailSender)
if err != nil { if err != nil {
log.Fatalf("main : Notify Email : %+v", err) log.Fatalf("main : Notify Email : %+v", err)
} }
@ -430,12 +433,44 @@ func main() {
} }
// ========================================================================= // =========================================================================
// Load middlewares that need to be configured specific for the service. // Init repositories and AppContext
var serviceMiddlewares = []web.Middleware{ projectRoute, err := project_route.New(cfg.Project.WebApiBaseUrl, cfg.Service.BaseUrl)
mid.Translator(webcontext.UniversalTranslator()), 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)
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,
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. // 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" { if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" {
redirect := mid.DomainNameRedirect(mid.DomainNameRedirectConfig{ redirect := mid.DomainNameRedirect(mid.DomainNameRedirectConfig{
@ -451,24 +486,23 @@ func main() {
DomainName: primaryServiceHost, DomainName: primaryServiceHost,
HTTPSEnabled: cfg.Service.EnableHTTPS, 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. // Generate the new session store and append it to the global list of middlewares.
// Init session store // Init session store
if cfg.Service.SessionName == "" { if cfg.Service.SessionName == "" {
cfg.Service.SessionName = fmt.Sprintf("%s-session", cfg.Service.Name) cfg.Service.SessionName = fmt.Sprintf("%s-session", cfg.Service.Name)
} }
sessionStore := sessions.NewCookieStore([]byte(cfg.Service.SecretKey)) sessionStore := sessions.NewCookieStore([]byte(cfg.Project.SharedSecretKey))
serviceMiddlewares = append(serviceMiddlewares, mid.Session(sessionStore, cfg.Service.SessionName)) appCtx.PostAppMiddleware = append(appCtx.PostAppMiddleware, mid.Session(sessionStore, cfg.Service.SessionName))
// ========================================================================= // =========================================================================
// URL Formatter // URL Formatter
projectRoutes, err := project_route.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 // s3UrlFormatter is a help function used by to convert an s3 key to
// a publicly available image URL. // a publicly available image URL.
@ -488,7 +522,7 @@ func main() {
return s3UrlFormatter(p) return s3UrlFormatter(p)
} }
} else { } else {
staticS3UrlFormatter = projectRoutes.WebAppUrl staticS3UrlFormatter = projectRoute.WebAppUrl
} }
// staticUrlFormatter is a help function used by template functions defined below. // staticUrlFormatter is a help function used by template functions defined below.
@ -691,7 +725,7 @@ func main() {
return nil return nil
} }
usr, err := user.ReadByID(ctx, auth.Claims{}, masterDb, claims.Subject) usr, err := usrRepo.ReadByID(ctx, auth.Claims{}, claims.Subject)
if err != nil { if err != nil {
return nil return nil
} }
@ -726,7 +760,7 @@ func main() {
return nil return nil
} }
acc, err := account.ReadByID(ctx, auth.Claims{}, masterDb, claims.Audience) acc, err := accRepo.ReadByID(ctx, auth.Claims{}, claims.Audience)
if err != nil { if err != nil {
return nil return nil
} }
@ -867,7 +901,7 @@ func main() {
enableHotReload := cfg.Env == "dev" enableHotReload := cfg.Env == "dev"
// Template Renderer used to generate HTML response for web experience. // 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 { if err != nil {
log.Fatalf("main : Marshalling Config to JSON : %+v", err) log.Fatalf("main : Marshalling Config to JSON : %+v", err)
} }
@ -919,7 +953,7 @@ func main() {
if cfg.HTTP.Host != "" { if cfg.HTTP.Host != "" {
api := http.Server{ api := http.Server{
Addr: cfg.HTTP.Host, Addr: cfg.HTTP.Host,
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, cfg.Service.SecretKey, notifyEmail, renderer, serviceMiddlewares...), Handler: handlers.APP(shutdown, appCtx),
ReadTimeout: cfg.HTTP.ReadTimeout, ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout, WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
@ -936,7 +970,7 @@ func main() {
if cfg.HTTPS.Host != "" { if cfg.HTTPS.Host != "" {
api := http.Server{ api := http.Server{
Addr: cfg.HTTPS.Host, Addr: cfg.HTTPS.Host,
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, cfg.Service.SecretKey, notifyEmail, renderer, serviceMiddlewares...), Handler: handlers.APP(shutdown, appCtx),
ReadTimeout: cfg.HTTPS.ReadTimeout, ReadTimeout: cfg.HTTPS.ReadTimeout,
WriteTimeout: cfg.HTTPS.WriteTimeout, WriteTimeout: cfg.HTTPS.WriteTimeout,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,

View File

@ -20,7 +20,7 @@ package notify
Username: cfg.SMTP.User, Username: cfg.SMTP.User,
Password: cfg.SMTP.Pass} Password: cfg.SMTP.Pass}
notifyEmail, err = notify.NewEmailSmtp(d, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender) notifyEmail, err = notify.NewEmailSmtp(d, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender)
*/ */
import ( import (
"context" "context"

View File

@ -272,3 +272,8 @@ func ParseResetHash(ctx context.Context, secretKey string, str string, now time.
return &hash, nil return &hash, nil
} }
// ParseResetHash extracts the details encrypted in the hash string.
func (repo *Repository) ParseResetHash(ctx context.Context, str string, now time.Time) (*ResetHash, error) {
return ParseResetHash(ctx, repo.secretKey, str, now)
}