diff --git a/cmd/web-api/ecs-task-definition.json b/cmd/web-api/ecs-task-definition.json index dc78985..59ffe40 100644 --- a/cmd/web-api/ecs-task-definition.json +++ b/cmd/web-api/ecs-task-definition.json @@ -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}"}, diff --git a/cmd/web-api/main.go b/cmd/web-api/main.go index f64f953..9a48ec4 100644 --- a/cmd/web-api/main.go +++ b/cmd/web-api/main.go @@ -97,7 +97,7 @@ func main() { DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"` } 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"` HostNames []string `envconfig:"HOST_NAMES" example:"alternative-subdomain.example.saasstartupkit.com"` EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"` @@ -106,7 +106,7 @@ func main() { ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` } Project struct { - Name string `default:"" envconfig:"PROJECT"` + 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"` @@ -203,7 +203,7 @@ func main() { 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...) } @@ -299,7 +299,7 @@ func main() { // 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") + secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "SharedSecretKey") // If AWS is enabled, check the Secrets Manager for the session key. if awsSession != nil { diff --git a/cmd/web-app/ecs-task-definition.json b/cmd/web-app/ecs-task-definition.json index 1132f7a..88b63ef 100644 --- a/cmd/web-app/ecs-task-definition.json +++ b/cmd/web-app/ecs-task-definition.json @@ -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}"}, diff --git a/cmd/web-app/handlers/account.go b/cmd/web-app/handlers/account.go index c2e3a0c..3ba3e0f 100644 --- a/cmd/web-app/handlers/account.go +++ b/cmd/web-app/handlers/account.go @@ -12,6 +12,7 @@ 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" + "geeks-accelerator/oss/saas-starter-kit/internal/user_auth" "github.com/gorilla/schema" "github.com/jmoiron/sqlx" "github.com/pkg/errors" @@ -19,9 +20,12 @@ import ( // Account represents the Account API method handler set. type Account struct { - MasterDB *sqlx.DB - Renderer web.Renderer - Authenticator *auth.Authenticator + AccountRepo *account.Repository + AccountPrefRepo *account_preference.Repository + AuthRepo *user_auth.Repository + Authenticator *auth.Authenticator + MasterDB *sqlx.DB + 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 } diff --git a/cmd/web-app/handlers/projects.go b/cmd/web-app/handlers/projects.go index d3bda68..8a375fe 100644 --- a/cmd/web-app/handlers/projects.go +++ b/cmd/web-app/handlers/projects.go @@ -13,16 +13,15 @@ import ( "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 *project.Repository + Redis *redis.Client + Renderer web.Renderer } func urlProjectsIndex() string { @@ -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) { - 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 +185,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 +250,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 +275,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 +319,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 +350,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 } diff --git a/cmd/web-app/handlers/root.go b/cmd/web-app/handlers/root.go index e4ea3e6..dad0360 100644 --- a/cmd/web-app/handlers/root.go +++ b/cmd/web-app/handlers/root.go @@ -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) diff --git a/cmd/web-app/handlers/routes.go b/cmd/web-app/handlers/routes.go index e30a2c8..01bd4b2 100644 --- a/cmd/web-app/handlers/routes.go +++ b/cmd/web-app/handlers/routes.go @@ -9,13 +9,20 @@ import ( "path/filepath" "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/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 +34,58 @@ const ( 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. -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 +99,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, + MasterDB: appCtx.MasterDB, + 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, + UserRepo: appCtx.UserRepo, + UserAccountRepo: appCtx.UserAccountRepo, + AccountRepo: appCtx.AccountRepo, + AuthRepo: appCtx.AuthRepo, + MasterDB: appCtx.MasterDB, + Renderer: appCtx.Renderer, } app.Handle("POST", "/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("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, + MasterDB: appCtx.MasterDB, + 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, + MasterDB: appCtx.MasterDB, + Renderer: appCtx.Renderer, } // This route is not authenticated 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. 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, + MasterDB: appCtx.MasterDB, + Redis: appCtx.Redis, } app.Handle("GET", "/geo/regions/autocomplete", g.RegionsAutocomplete) app.Handle("GET", "/geo/postal_codes/autocomplete", g.PostalCodesAutocomplete) @@ -168,17 +207,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 +231,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 +247,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 diff --git a/cmd/web-app/handlers/signup.go b/cmd/web-app/handlers/signup.go index f24a175..cb23c48 100644 --- a/cmd/web-app/handlers/signup.go +++ b/cmd/web-app/handlers/signup.go @@ -20,9 +20,10 @@ import ( // Signup represents the Signup API method handler set. type Signup struct { - MasterDB *sqlx.DB - Renderer web.Renderer - Authenticator *auth.Authenticator + SignupRepo *signup.Repository + AuthRepo *user_auth.Repository + MasterDB *sqlx.DB + Renderer web.Renderer } // 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. - _, 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 +69,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 +78,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 } diff --git a/cmd/web-app/handlers/user.go b/cmd/web-app/handlers/user.go index c91a3d3..b47b4c9 100644 --- a/cmd/web-app/handlers/user.go +++ b/cmd/web-app/handlers/user.go @@ -11,11 +11,9 @@ import ( "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" @@ -27,12 +25,12 @@ 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 + UserRepo *user.Repository + AuthRepo *user_auth.Repository + UserAccountRepo *user_account.Repository + AccountRepo *account.Repository + MasterDB *sqlx.DB + Renderer web.Renderer } 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. - 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 +95,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 } @@ -173,7 +171,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: @@ -238,7 +236,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 +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. - 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 +269,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 } @@ -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) } - _, err = user.ParseResetHash(ctx, h.SecretKey, resetHash, ctxValues.Now) + _, err = h.UserRepo.ParseResetHash(ctx, resetHash, ctxValues.Now) if err != nil { switch errors.Cause(err) { case user.ErrResetExpired: @@ -328,14 +326,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 } @@ -388,7 +386,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 +407,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 +439,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 } @@ -484,7 +482,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 } @@ -551,7 +549,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 +563,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 +586,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 +610,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, @@ -657,7 +655,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 +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. 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 +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.", 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 +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.", 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 } @@ -757,7 +755,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 +769,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 +792,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 +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. -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.") } diff --git a/cmd/web-app/handlers/users.go b/cmd/web-app/handlers/users.go index e0b9526..86e5f8b 100644 --- a/cmd/web-app/handlers/users.go +++ b/cmd/web-app/handlers/users.go @@ -3,14 +3,16 @@ package handlers import ( "context" "fmt" + "net/http" + "strings" + "time" + "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/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" @@ -20,20 +22,17 @@ import ( "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 *user.Repository + UserAccountRepo *user_account.Repository + AuthRepo *user_auth.Repository + InviteRepo *invite.Repository + MasterDB *sqlx.DB + Redis *redis.Client + Renderer web.Renderer } 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) { - 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 +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 { switch errors.Cause(err) { default: @@ -246,7 +245,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, @@ -327,7 +326,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 +351,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 +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 { switch errors.Cause(err) { default: @@ -439,7 +438,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 +464,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 +496,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, }) @@ -577,7 +576,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 +660,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 +698,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 +719,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 +728,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 +775,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 } diff --git a/cmd/web-app/main.go b/cmd/web-app/main.go index a89843f..659d576 100644 --- a/cmd/web-app/main.go +++ b/cmd/web-app/main.go @@ -6,7 +6,12 @@ import ( "encoding/json" "expvar" "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" "log" "net" @@ -33,7 +38,7 @@ 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" @@ -52,7 +57,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. @@ -67,10 +71,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 @@ -88,12 +91,12 @@ func main() { DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"` } Service struct { - Name string `default:"web-app" envconfig:"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 { + 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"` @@ -105,11 +108,11 @@ func main() { ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` } Project struct { - Name string `default:"" envconfig:"PROJECT"` - 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"` + 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"` @@ -202,7 +205,7 @@ func main() { 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...) } @@ -298,7 +301,7 @@ func main() { // 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") + secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "SharedSecretKey") // If AWS is enabled, check the Secrets Manager for the session key. if awsSession != nil { @@ -310,10 +313,10 @@ func main() { // If the session key is still empty, generate a new key. if cfg.Project.SharedSecretKey == "" { - cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32)) + cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32)) if awsSession != nil { - err = devops.SecretManagerPutString(awsSession, secretID, cfg.Service.SecretKey) + err = devops.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey) if err != nil { log.Fatalf("main : Session : %+v", err) } @@ -396,7 +399,7 @@ func main() { 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.Service.SharedTemplateDir, cfg.Service.EmailSender) + notifyEmail, err = notify.NewEmailAws(awsSession, cfg.Project.SharedTemplateDir, cfg.Project.EmailSender) if err != nil { 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{ - 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) + 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. if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" { redirect := mid.DomainNameRedirect(mid.DomainNameRedirectConfig{ @@ -451,24 +486,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) } - sessionStore := sessions.NewCookieStore([]byte(cfg.Service.SecretKey)) - 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_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 // a publicly available image URL. @@ -488,7 +522,7 @@ func main() { return s3UrlFormatter(p) } } else { - staticS3UrlFormatter = projectRoutes.WebAppUrl + staticS3UrlFormatter = projectRoute.WebAppUrl } // staticUrlFormatter is a help function used by template functions defined below. @@ -691,7 +725,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 } @@ -726,7 +760,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 } @@ -867,7 +901,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) } @@ -919,7 +953,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.SecretKey, notifyEmail, renderer, serviceMiddlewares...), + Handler: handlers.APP(shutdown, appCtx), ReadTimeout: cfg.HTTP.ReadTimeout, WriteTimeout: cfg.HTTP.WriteTimeout, MaxHeaderBytes: 1 << 20, @@ -936,7 +970,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.SecretKey, notifyEmail, renderer, serviceMiddlewares...), + Handler: handlers.APP(shutdown, appCtx), ReadTimeout: cfg.HTTPS.ReadTimeout, WriteTimeout: cfg.HTTPS.WriteTimeout, MaxHeaderBytes: 1 << 20, diff --git a/internal/platform/notify/email_smtp.go b/internal/platform/notify/email_smtp.go index ea56c4a..9be78c8 100644 --- a/internal/platform/notify/email_smtp.go +++ b/internal/platform/notify/email_smtp.go @@ -20,7 +20,7 @@ package notify Username: cfg.SMTP.User, Password: cfg.SMTP.Pass} notifyEmail, err = notify.NewEmailSmtp(d, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender) - */ +*/ import ( "context" diff --git a/internal/user/models.go b/internal/user/models.go index 7860dd3..b0c3cb7 100644 --- a/internal/user/models.go +++ b/internal/user/models.go @@ -272,3 +272,8 @@ func ParseResetHash(ctx context.Context, secretKey string, str string, now time. 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) +}