diff --git a/README.md b/README.md index f7abc02..f5a3af1 100644 --- a/README.md +++ b/README.md @@ -381,7 +381,7 @@ schema migrations before running any unit tests. To login to the local Postgres container, use the following command: ```bash docker exec -it saas-starter-kit_postgres_1 /bin/bash -bash-4.4# psql -u postgres shared +bash-4.4# psql -U postgres shared ``` The example project currently only includes a few tables. As more functionality is built into both the web-app and diff --git a/cmd/web-api/handlers/routes.go b/cmd/web-api/handlers/routes.go index 84527c2..df88209 100644 --- a/cmd/web-api/handlers/routes.go +++ b/cmd/web-api/handlers/routes.go @@ -1,6 +1,7 @@ package handlers import ( + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "log" "net/http" "os" @@ -15,7 +16,7 @@ import ( ) // API returns a handler for a set of routes. -func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler { +func API(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler { // Define base middlewares applied to all requests. middlewares := []web.Middleware{ @@ -43,14 +44,14 @@ func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.D MasterDB: masterDB, TokenGenerator: authenticator, } - app.Handle("GET", "/v1/users", u.Find, mid.Authenticate(authenticator)) - app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("GET", "/v1/users/:id", u.Read, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/users", u.Update, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.Authenticate(authenticator)) + app.Handle("GET", "/v1/users", u.Find, mid.AuthenticateHeader(authenticator)) + app.Handle("POST", "/v1/users", u.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/users/:id", u.Read, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/users", u.Update, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateHeader(authenticator)) // This route is not authenticated app.Handle("POST", "/v1/oauth/token", u.Token) @@ -59,19 +60,19 @@ func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.D ua := UserAccount{ MasterDB: masterDB, } - app.Handle("GET", "/v1/user_accounts", ua.Find, mid.Authenticate(authenticator)) - app.Handle("POST", "/v1/user_accounts", ua.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("GET", "/v1/user_accounts/:id", ua.Read, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/user_accounts", ua.Find, mid.AuthenticateHeader(authenticator)) + app.Handle("POST", "/v1/user_accounts", ua.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/user_accounts/:id", ua.Read, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) // Register account endpoints. a := Account{ MasterDB: masterDB, } - app.Handle("GET", "/v1/accounts/:id", a.Read, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/accounts", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/accounts/:id", a.Read, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/accounts", a.Update, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) // Register signup endpoints. s := Signup{ @@ -83,12 +84,12 @@ func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.D p := Project{ MasterDB: masterDB, } - app.Handle("GET", "/v1/projects", p.Find, mid.Authenticate(authenticator)) - app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("GET", "/v1/projects/:id", p.Read, mid.Authenticate(authenticator)) - app.Handle("PATCH", "/v1/projects", p.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/projects", p.Find, mid.AuthenticateHeader(authenticator)) + app.Handle("POST", "/v1/projects", p.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/projects/:id", p.Read, mid.AuthenticateHeader(authenticator)) + app.Handle("PATCH", "/v1/projects", p.Update, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) // Register swagger documentation. // TODO: Add authentication. Current authenticator requires an Authorization header diff --git a/cmd/web-api/main.go b/cmd/web-api/main.go index 05febd9..d3b67aa 100644 --- a/cmd/web-api/main.go +++ b/cmd/web-api/main.go @@ -91,6 +91,7 @@ func main() { HostNames []string `envconfig:"HOST_NAMES" example:"alternative-subdomain.eproc.tech"` EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"` TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"` + WebAppBaseUrl string `default:"http://127.0.0.1:3000" envconfig:"WEB_APP_BASE_URL" example:"www.eproc.tech"` DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"` ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` } diff --git a/cmd/web-app/handlers/root.go b/cmd/web-app/handlers/root.go index 366702f..58c3373 100644 --- a/cmd/web-app/handlers/root.go +++ b/cmd/web-app/handlers/root.go @@ -2,32 +2,63 @@ package handlers import ( "context" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" + "fmt" "net/http" + "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" "github.com/jmoiron/sqlx" ) -// User represents the User API method handler set. +// Root represents the Root API method handler set. type Root struct { - MasterDB *sqlx.DB - Renderer web.Renderer - // ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE. + MasterDB *sqlx.DB + Renderer web.Renderer + ProjectRoutes project_routes.ProjectRoutes } -// List returns all the existing users in the system. -func (u *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { +// Index determines if the user has authentication and loads the associated page. +func (h *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { - // Force users to login to access the index page. - if claims, err := auth.ClaimsFromContext(ctx); err != nil || !claims.HasAuth() { - http.Redirect(w, r, "/user/login", http.StatusFound) - return nil + if claims, err := auth.ClaimsFromContext(ctx); err == nil && claims.HasAuth() { + return h.indexDashboard(ctx, w, r, params) } + return h.indexDefault(ctx, w, r, params) +} + +// indexDashboard loads the dashboard for a user when they are authenticated. +func (h *Root) indexDashboard(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { data := map[string]interface{}{ "imgSizes": []int{100, 200, 300, 400, 500}, } - return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "root-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) + return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "root-dashboard.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} + +// indexDefault loads the root index page when a user has no authentication. +func (u *Root) indexDefault(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + http.Redirect(w, r, "/user/login", http.StatusFound) + return nil +} + +// IndexHtml redirects /index.html to the website root page. +func (u *Root) IndexHtml(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + http.Redirect(w, r, "/", http.StatusMovedPermanently) + return nil +} + +// RobotHandler returns a robots.txt response. +func (h *Root) RobotTxt(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + if webcontext.ContextEnv(ctx) != webcontext.Env_Prod { + txt := "User-agent: *\nDisallow: /" + return web.RespondText(ctx, w, txt, http.StatusOK) + } + + sitemapUrl := h.ProjectRoutes.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 93c00e3..3c9876e 100644 --- a/cmd/web-app/handlers/routes.go +++ b/cmd/web-app/handlers/routes.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" + project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes" "github.com/jmoiron/sqlx" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" ) @@ -22,7 +23,7 @@ const ( ) // 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, renderer web.Renderer, globalMids ...web.Middleware) http.Handler { +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, renderer web.Renderer, globalMids ...web.Middleware) http.Handler { // Define base middlewares applied to all requests. middlewares := []web.Middleware{ @@ -42,7 +43,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir MasterDB: masterDB, Renderer: renderer, } - app.Handle("GET", "/projects", p.Index, mid.HasAuth()) + app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth()) // Register user management and authentication endpoints. u := User{ @@ -69,12 +70,14 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir // Register root r := Root{ - MasterDB: masterDB, - Renderer: renderer, + MasterDB: masterDB, + Renderer: renderer, + ProjectRoutes: projectRoutes, } // This route is not authenticated - app.Handle("GET", "/index.html", r.Index) - app.Handle("GET", "/", r.Index) + app.Handle("GET", "/", r.Index, mid.AuthenticateSessionOptional(authenticator)) + app.Handle("GET", "/index.html", r.IndexHtml) + app.Handle("GET", "/robots.txt", r.RobotTxt) // Register health check endpoint. This route is not authenticated. check := Check{ @@ -84,6 +87,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir } 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, "") if err != nil { diff --git a/cmd/web-app/handlers/signup.go b/cmd/web-app/handlers/signup.go index b25cf7f..802ff83 100644 --- a/cmd/web-app/handlers/signup.go +++ b/cmd/web-app/handlers/signup.go @@ -27,6 +27,11 @@ type Signup struct { // Step1 handles collecting the first detailed needed to create a new account. func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + ctxValues, err := webcontext.ContextValues(ctx) + if err != nil { + return err + } + // req := new(signup.SignupRequest) data := make(map[string]interface{}) @@ -46,7 +51,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque } // Execute the account / user signup. - res, err := signup.Signup(ctx, claims, h.MasterDB, *req, time.Now()) + _, err = signup.Signup(ctx, claims, h.MasterDB, *req, ctxValues.Now) if err != nil { switch errors.Cause(err) { case account.ErrForbidden: @@ -54,21 +59,27 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque default: if verr, ok := weberror.NewValidationError(ctx, err); ok { data["validationErrors"] = verr.(*weberror.Error) + return nil } else { return err } } - } else { - // Authenticated the new user. - userAuth, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, res.User.Email, req.User.Password, time.Hour, time.Now()) - if err != nil { - return err - } - - _ = userAuth.Expiry - _ = userAuth.AccessToken } + // Authenticated the new user. + token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.User.Email, req.User.Password, time.Hour, ctxValues.Now) + if err != nil { + return err + } + + // Add the token to the users session. + err = handleSessionToken(ctx, w, r, token) + if err != nil { + return err + } + + // Redirect the user to the dashboard. + http.Redirect(w, r, "/", http.StatusFound) } return nil diff --git a/cmd/web-app/handlers/user.go b/cmd/web-app/handlers/user.go index 1d385a7..d2adb37 100644 --- a/cmd/web-app/handlers/user.go +++ b/cmd/web-app/handlers/user.go @@ -2,11 +2,20 @@ package handlers import ( "context" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "net/http" + "time" + "geeks-accelerator/oss/saas-starter-kit/internal/account" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" + "geeks-accelerator/oss/saas-starter-kit/internal/user" + "github.com/gorilla/schema" + "github.com/gorilla/sessions" "github.com/jmoiron/sqlx" + "github.com/pborman/uuid" + "github.com/pkg/errors" ) // User represents the User API method handler set. @@ -16,20 +25,138 @@ type User struct { Authenticator *auth.Authenticator } -// List returns all the existing users in the system. -func (u *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { - - return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) +type UserLoginRequest struct { + user.AuthenticateRequest + RememberMe bool } // List returns all the existing users in the system. -func (u *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { +func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { - return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-logout.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) + ctxValues, err := webcontext.ContextValues(ctx) + if err != nil { + return err + } + + // + req := new(UserLoginRequest) + data := make(map[string]interface{}) + f := func() error { + + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + return err + } + + decoder := schema.NewDecoder() + if err := decoder.Decode(req, r.PostForm); err != nil { + return err + } + + if err := webcontext.Validator().Struct(req); err != nil { + if ne, ok := weberror.NewValidationError(ctx, err); ok { + data["validationErrors"] = ne.(*weberror.Error) + return nil + } else { + return err + } + } + + sessionTTL := time.Hour + if req.RememberMe { + sessionTTL = time.Hour * 36 + } + + // Authenticated the user. + token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.Email, req.Password, sessionTTL, ctxValues.Now) + if err != nil { + switch errors.Cause(err) { + case account.ErrForbidden: + return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden)) + default: + if verr, ok := weberror.NewValidationError(ctx, err); ok { + data["validationErrors"] = verr.(*weberror.Error) + return nil + } else { + return err + } + } + } + + // Add the token to the users session. + err = handleSessionToken(ctx, w, r, token) + if err != nil { + return err + } + + // Redirect the user to the dashboard. + http.Redirect(w, r, "/", http.StatusFound) + } + + return nil + } + + if err := f(); err != nil { + return web.RenderError(ctx, w, r, err, h.Renderer, tmplLayoutBase, tmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8) + } + + data["form"] = req + + if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(UserLoginRequest{})); ok { + data["validationDefaults"] = verr.(*weberror.Error) + } + + return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} + +// handleSessionToken persists the access token to the session for request authentication. +func handleSessionToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token user.Token) error { + if token.AccessToken == "" { + return errors.New("accessToken is required.") + } + + sess := webcontext.ContextSession(ctx) + + if sess.IsNew { + sess.ID = uuid.NewRandom().String() + } + + sess.Options = &sessions.Options{ + Path: "/", + MaxAge: int(token.TTL.Seconds()), + HttpOnly: false, + } + + sess = webcontext.SessionWithAccessToken(sess, token.AccessToken) + + if err := sess.Save(r, w); err != nil { + return err + } + + return nil +} + +// Logout handles removing authentication for the user. +func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + sess := webcontext.ContextSession(ctx) + + // Set the access token to empty to logout the user. + sess = webcontext.SessionWithAccessToken(sess, "") + + if err := sess.Save(r, w); err != nil { + return err + } + + // Redirect the user to the root page. + http.Redirect(w, r, "/", http.StatusFound) + + return nil } // List returns all the existing users in the system. -func (u *User) ForgotPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { +func (h *User) ForgotPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { - return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-forgot-password.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) + return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-forgot-password.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) } diff --git a/cmd/web-app/main.go b/cmd/web-app/main.go index b31368c..87fb116 100644 --- a/cmd/web-app/main.go +++ b/cmd/web-app/main.go @@ -6,9 +6,6 @@ import ( "encoding/json" "expvar" "fmt" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" "html/template" "log" "net" @@ -25,18 +22,25 @@ import ( "geeks-accelerator/oss/saas-starter-kit/cmd/web-app/handlers" "geeks-accelerator/oss/saas-starter-kit/internal/mid" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/devops" "geeks-accelerator/oss/saas-starter-kit/internal/platform/flag" img_resize "geeks-accelerator/oss/saas-starter-kit/internal/platform/img-resize" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" 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" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" "github.com/go-redis/redis" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" "github.com/kelseyhightower/envconfig" "github.com/lib/pq" + "github.com/pkg/errors" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws" @@ -90,6 +94,9 @@ func main() { CloudFrontEnabled bool `envconfig:"CLOUDFRONT_ENABLED"` ImgResizeEnabled bool `envconfig:"IMG_RESIZE_ENABLED"` } + WebApiBaseUrl string `default:"http://127.0.0.1:3001" envconfig:"WEB_API_BASE_URL" example:"http://api.eproc.tech"` + SessionKey string `default:"" envconfig:"SESSION_KEY"` + SessionName string `default:"" envconfig:"SESSION_NAME"` DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"` ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` } @@ -382,8 +389,50 @@ func main() { serviceMiddlewares = append(serviceMiddlewares, redirect) } + // Init session store + if cfg.Service.SessionName == "" { + cfg.Service.SessionName = fmt.Sprintf("%s-session", cfg.Service.Name) + } + + // Set the session key if not provided in the config. + if cfg.Service.SessionKey == "" { + + // AWS secrets manager ID for storing the session key. This is optional and only will be used + // if a valid AWS session is provided. + secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "session") + + // If AWS is enabled, check the Secrets Manager for the session key. + if awsSession != nil { + cfg.Service.SessionKey, err = devops.SecretManagerGetString(awsSession, secretID) + if err != nil && errors.Cause(err) != devops.ErrSecreteNotFound { + log.Fatalf("main : Session : %+v", err) + } + } + + // If the session key is still empty, generate a new key. + if cfg.Service.SessionKey == "" { + cfg.Service.SessionKey = string(securecookie.GenerateRandomKey(32)) + + if awsSession != nil { + err = devops.SecretManagerPutString(awsSession, secretID, cfg.Service.SessionKey) + if err != nil { + log.Fatalf("main : Session : %+v", err) + } + } + } + } + + // Generate the new session store and append it to the global list of middlewares. + sessionStore := sessions.NewCookieStore([]byte(cfg.Service.SessionKey)) + serviceMiddlewares = append(serviceMiddlewares, mid.Session(sessionStore, cfg.Service.SessionName)) + // ========================================================================= // URL Formatter + projectRoutes, err := project_routes.New(cfg.Service.WebApiBaseUrl, cfg.Service.BaseUrl) + if err != nil { + log.Fatalf("main : project routes : %+v", cfg.Service.BaseUrl, err) + } + // s3UrlFormatter is a help function used by to convert an s3 key to // a publicly available image URL. var staticS3UrlFormatter func(string) string @@ -402,15 +451,7 @@ func main() { return s3UrlFormatter(p) } } else { - baseUrl, err := url.Parse(cfg.Service.BaseUrl) - if err != nil { - log.Fatalf("main : url Parse(%s) : %+v", cfg.Service.BaseUrl, err) - } - - staticS3UrlFormatter = func(p string) string { - baseUrl.Path = p - return baseUrl.String() - } + staticS3UrlFormatter = projectRoutes.WebAppUrl } // staticUrlFormatter is a help function used by template functions defined below. @@ -735,7 +776,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, renderer, serviceMiddlewares...), + Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, renderer, serviceMiddlewares...), ReadTimeout: cfg.HTTP.ReadTimeout, WriteTimeout: cfg.HTTP.WriteTimeout, MaxHeaderBytes: 1 << 20, @@ -752,7 +793,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, renderer, serviceMiddlewares...), + Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, renderer, serviceMiddlewares...), ReadTimeout: cfg.HTTPS.ReadTimeout, WriteTimeout: cfg.HTTPS.WriteTimeout, MaxHeaderBytes: 1 << 20, diff --git a/cmd/web-app/templates/content/root-index.tmpl b/cmd/web-app/templates/content/root-dashboard.gohtml similarity index 100% rename from cmd/web-app/templates/content/root-index.tmpl rename to cmd/web-app/templates/content/root-dashboard.gohtml diff --git a/cmd/web-app/templates/content/user-login.tmpl b/cmd/web-app/templates/content/user-login.tmpl index a3295c4..fa8546f 100644 --- a/cmd/web-app/templates/content/user-login.tmpl +++ b/cmd/web-app/templates/content/user-login.tmpl @@ -20,22 +20,24 @@