You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-07-01 00:55:01 +02:00
Completed user login with session authentication
This commit is contained in:
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -20,22 +20,24 @@
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
|
||||
</div>
|
||||
<form class="user">
|
||||
<form class="user" method="post" novalidate>
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control form-control-user" id="loginEmail" aria-describedby="emailHelp" placeholder="Enter Email Address...">
|
||||
<input type="email" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "AuthenticateRequest.Email" }}" name="Email" value="{{ $.form.Email }}" placeholder="Enter Email Address...">
|
||||
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "AuthenticateRequest.Email" }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control form-control-user" id="loginPassword" placeholder="Password">
|
||||
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "AuthenticateRequest.Password" }}" name="Password" value="{{ $.form.Password }}" placeholder="Password">
|
||||
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "AuthenticateRequest.Password" }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox small">
|
||||
<input type="checkbox" class="custom-control-input" id="customCheck">
|
||||
<label class="custom-control-label" for="customCheck">Remember Me</label>
|
||||
<input type="checkbox" class="custom-control-input" id="checkRemberMe" name="RememberMe" value="1" {{ if $.form.RememberMe }}checked="checked"{{end}}>
|
||||
<label class="custom-control-label" for="checkRemberMe">Remember Me</label>
|
||||
</div>
|
||||
</div>
|
||||
<a href="index.html" class="btn btn-primary btn-user btn-block">
|
||||
<button class="btn btn-primary btn-user btn-block">
|
||||
Login
|
||||
</a>
|
||||
</button>
|
||||
<hr>
|
||||
</form>
|
||||
<hr>
|
||||
|
@ -59,7 +59,7 @@
|
||||
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
|
||||
<a class="btn btn-primary" href="/logout">Logout</a>
|
||||
<a class="btn btn-primary" href="/user/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
2
go.mod
2
go.mod
@ -20,6 +20,8 @@ require (
|
||||
github.com/google/go-cmp v0.3.0
|
||||
github.com/google/uuid v1.1.1 // indirect
|
||||
github.com/gorilla/schema v1.1.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/huandu/go-sqlbuilder v1.4.1
|
||||
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
|
3
go.sum
3
go.sum
@ -71,6 +71,9 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/go-sqlbuilder v1.4.1 h1:DYGFGLbOUXhtQ2kwO1uyDIPJbsztmVWdPPDyxi0EJGw=
|
||||
|
@ -2,12 +2,13 @@ package mid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
)
|
||||
@ -22,17 +23,18 @@ func ErrorForbidden(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Authenticate validates a JWT from the `Authorization` header.
|
||||
func Authenticate(authenticator *auth.Authenticator) web.Middleware {
|
||||
func AuthenticateHeader(authenticator *auth.Authenticator) web.Middleware {
|
||||
|
||||
// This is the actual middleware function to be executed.
|
||||
f := func(after web.Handler) web.Handler {
|
||||
|
||||
// Wrap this handler around the next one provided.
|
||||
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Authenticate")
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.AuthenticateHeader")
|
||||
defer span.Finish()
|
||||
|
||||
m := func() error {
|
||||
|
||||
authHdr := r.Header.Get("Authorization")
|
||||
if authHdr == "" {
|
||||
err := errors.New("missing Authorization header")
|
||||
@ -71,6 +73,65 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
|
||||
return f
|
||||
}
|
||||
|
||||
// AuthenticateSessionRequired requires a JWT access token to be loaded from the session.
|
||||
func AuthenticateSessionRequired(authenticator *auth.Authenticator) web.Middleware {
|
||||
return authenticateSession(authenticator, true)
|
||||
}
|
||||
|
||||
// AuthenticateSessionOptional loads a JWT access token from the session if it exists.
|
||||
func AuthenticateSessionOptional(authenticator *auth.Authenticator) web.Middleware {
|
||||
return authenticateSession(authenticator, false)
|
||||
}
|
||||
|
||||
// authenticateSession validates a JWT by the loading the access token from the session.
|
||||
func authenticateSession(authenticator *auth.Authenticator, required bool) web.Middleware {
|
||||
|
||||
// This is the actual middleware function to be executed.
|
||||
f := func(after web.Handler) web.Handler {
|
||||
|
||||
// Wrap this handler around the next one provided.
|
||||
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.AuthenticateSession")
|
||||
defer span.Finish()
|
||||
|
||||
m := func() error {
|
||||
tknStr, ok := webcontext.ContextAccessToken(ctx)
|
||||
if !ok || tknStr == "" {
|
||||
if required {
|
||||
err := errors.New("missing AccessToken from session")
|
||||
return weberror.NewError(ctx, err, http.StatusUnauthorized)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
claims, err := authenticator.ParseClaims(tknStr)
|
||||
if err != nil {
|
||||
return weberror.NewError(ctx, err, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// Add claims to the context so they can be retrieved later.
|
||||
ctx = context.WithValue(ctx, auth.Key, claims)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m(); err != nil {
|
||||
if web.RequestIsJson(r) {
|
||||
return web.RespondJsonError(ctx, w, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return after(ctx, w, r, params)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// HasAuth validates the current user is an authenticated user,
|
||||
func HasAuth() web.Middleware {
|
||||
|
||||
|
37
internal/mid/session.go
Normal file
37
internal/mid/session.go
Normal file
@ -0,0 +1,37 @@
|
||||
package mid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"net/http"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"github.com/gorilla/sessions"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
)
|
||||
|
||||
func Session(store sessions.Store, sessionName string) web.Middleware {
|
||||
|
||||
// This is the actual middleware function to be executed.
|
||||
f := func(after web.Handler) web.Handler {
|
||||
|
||||
// Wrap this handler around the next one provided.
|
||||
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Session")
|
||||
defer span.Finish()
|
||||
|
||||
// Get a session. We're ignoring the error resulted from decoding an
|
||||
// existing session: Get() always returns a session, even if empty.
|
||||
session, _ := store.Get(r, sessionName)
|
||||
|
||||
// Append the session to the context.
|
||||
ctx = webcontext.ContextWithSession(ctx, session)
|
||||
|
||||
return after(ctx, w, r, params)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
@ -6,7 +6,6 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||
"github.com/pkg/errors"
|
||||
@ -46,25 +45,20 @@ func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byt
|
||||
}
|
||||
}
|
||||
|
||||
svc := secretsmanager.New(c.awsSession)
|
||||
|
||||
secretID := filepath.Join(c.secretPrefix, key)
|
||||
|
||||
// Load the secret by ID from Secrets Manager.
|
||||
res, err := svc.GetSecretValue(&secretsmanager.GetSecretValueInput{
|
||||
SecretId: aws.String(secretID),
|
||||
})
|
||||
res, err := SecretManagerGetString(c.awsSession, secretID)
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok && (aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException || aerr.Code() == secretsmanager.ErrCodeInvalidRequestException) {
|
||||
if err == ErrSecreteNotFound {
|
||||
return nil, autocert.ErrCacheMiss
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to get value for secret id %s", secretID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("AWS Secrets Manager : Secret %s found", secretID)
|
||||
|
||||
return []byte(*res.SecretString), nil
|
||||
return []byte(res), nil
|
||||
}
|
||||
|
||||
// Put stores the data in the cache under the specified key.
|
||||
@ -72,48 +66,15 @@ func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byt
|
||||
// as long as the reverse operation, Get, results in the original data.
|
||||
func (c *SecretManagerAutocertCache) Put(ctx context.Context, key string, data []byte) error {
|
||||
|
||||
svc := secretsmanager.New(c.awsSession)
|
||||
|
||||
secretID := filepath.Join(c.secretPrefix, key)
|
||||
|
||||
// Create the new entry in AWS Secret Manager for the file.
|
||||
_, err := svc.CreateSecret(&secretsmanager.CreateSecretInput{
|
||||
Name: aws.String(secretID),
|
||||
SecretString: aws.String(string(data)),
|
||||
})
|
||||
err := SecretManagerPutString(c.awsSession, secretID, string(data))
|
||||
if err != nil {
|
||||
aerr, ok := err.(awserr.Error)
|
||||
|
||||
if ok && aerr.Code() == secretsmanager.ErrCodeInvalidRequestException {
|
||||
// InvalidRequestException: You can't create this secret because a secret with this
|
||||
// name is already scheduled for deletion.
|
||||
|
||||
// Restore secret after it was already previously deleted.
|
||||
_, err = svc.RestoreSecret(&secretsmanager.RestoreSecretInput{
|
||||
SecretId: aws.String(secretID),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "autocert failed to restore secret %s", secretID)
|
||||
}
|
||||
|
||||
} else if !ok || aerr.Code() != secretsmanager.ErrCodeResourceExistsException {
|
||||
return errors.Wrapf(err, "autocert failed to create secret %s", secretID)
|
||||
}
|
||||
|
||||
// If where was a resource exists error for create, then need to update the secret instead.
|
||||
_, err = svc.UpdateSecret(&secretsmanager.UpdateSecretInput{
|
||||
SecretId: aws.String(secretID),
|
||||
SecretString: aws.String(string(data)),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "autocert failed to update secret %s", secretID)
|
||||
}
|
||||
|
||||
log.Printf("AWS Secrets Manager : Secret %s updated", secretID)
|
||||
} else {
|
||||
log.Printf("AWS Secrets Manager : Secret %s created", secretID)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("AWS Secrets Manager : Secret %s updated", secretID)
|
||||
|
||||
if c.cache != nil {
|
||||
err = c.cache.Put(ctx, key, data)
|
||||
if err != nil {
|
||||
|
76
internal/platform/devops/secrets_manager.go
Normal file
76
internal/platform/devops/secrets_manager.go
Normal file
@ -0,0 +1,76 @@
|
||||
package devops
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrSecreteNotFound = errors.New("secret not found")
|
||||
|
||||
// SecretManagerGetString loads a key from AWS Secrets Manager.
|
||||
// when UnrecognizedClientException its likely the AWS IAM permissions are not correct.
|
||||
func SecretManagerGetString(awsSession *session.Session, secretID string) (string, error) {
|
||||
|
||||
svc := secretsmanager.New(awsSession)
|
||||
|
||||
// Load the secret by ID from Secrets Manager.
|
||||
res, err := svc.GetSecretValue(&secretsmanager.GetSecretValueInput{
|
||||
SecretId: aws.String(secretID),
|
||||
})
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok && (aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException || aerr.Code() == secretsmanager.ErrCodeInvalidRequestException) {
|
||||
return "", ErrSecreteNotFound
|
||||
}
|
||||
|
||||
return "", errors.Wrapf(err, "failed to get value for secret id %s", secretID)
|
||||
}
|
||||
|
||||
return *res.SecretString, nil
|
||||
}
|
||||
|
||||
// SecretManagerPutString saves a value to AWS Secrets Manager.
|
||||
// If the secret ID does not exist, it will create it.
|
||||
// If the secret ID was deleted, it will restore it and then update the value.
|
||||
func SecretManagerPutString(awsSession *session.Session, secretID, value string) error {
|
||||
|
||||
svc := secretsmanager.New(awsSession)
|
||||
|
||||
// Create the new entry in AWS Secret Manager for the file.
|
||||
_, err := svc.CreateSecret(&secretsmanager.CreateSecretInput{
|
||||
Name: aws.String(secretID),
|
||||
SecretString: aws.String(value),
|
||||
})
|
||||
if err != nil {
|
||||
aerr, ok := err.(awserr.Error)
|
||||
|
||||
if ok && aerr.Code() == secretsmanager.ErrCodeInvalidRequestException {
|
||||
// InvalidRequestException: You can't create this secret because a secret with this
|
||||
// name is already scheduled for deletion.
|
||||
|
||||
// Restore secret after it was already previously deleted.
|
||||
_, err = svc.RestoreSecret(&secretsmanager.RestoreSecretInput{
|
||||
SecretId: aws.String(secretID),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to restore secret %s", secretID)
|
||||
}
|
||||
|
||||
} else if !ok || aerr.Code() != secretsmanager.ErrCodeResourceExistsException {
|
||||
return errors.Wrapf(err, "failed to create secret %s", secretID)
|
||||
}
|
||||
|
||||
// If where was a resource exists error for create, then need to update the secret instead.
|
||||
_, err = svc.UpdateSecret(&secretsmanager.UpdateSecretInput{
|
||||
SecretId: aws.String(secretID),
|
||||
SecretString: aws.String(value),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to update secret %s", secretID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -96,7 +96,7 @@ func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, s
|
||||
// RespondError sends an error back to the client as plain text with
|
||||
// the status code 500 Internal Service Error
|
||||
func RespondError(ctx context.Context, w http.ResponseWriter, er error) error {
|
||||
return RespondErrorStatus(ctx, w, er, 0)
|
||||
return RespondErrorStatus(ctx, w, er, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// RespondErrorStatus sends an error back to the client as plain text with
|
||||
|
@ -218,7 +218,7 @@ func NewTemplateRenderer(templateDir string, enableHotReload bool, globalViewDat
|
||||
|
||||
// Main template used to render execute all templates against.
|
||||
r.mainTemplate = template.New("main")
|
||||
r.mainTemplate, _ = r.mainTemplate.Parse(`{{define "main" }} {{ template "base" . }} {{ end }}`)
|
||||
r.mainTemplate, _ = r.mainTemplate.Parse(`{{define "main" }}{{ template "base" . }}{{ end }}`)
|
||||
r.mainTemplate.Funcs(tmpl.Funcs)
|
||||
|
||||
// Ensure all layout files render successfully with no errors.
|
||||
|
@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
// ctxKey represents the type of value for the context key.
|
||||
type ctxKey int
|
||||
type ctxKeyValues int
|
||||
|
||||
// KeyValues is how request values or stored/retrieved.
|
||||
const KeyValues ctxKey = 1
|
||||
const KeyValues ctxKeyValues = 1
|
||||
|
||||
var ErrContextRequired = errors.New("web value missing from context")
|
||||
|
||||
|
48
internal/platform/web/webcontext/session.go
Normal file
48
internal/platform/web/webcontext/session.go
Normal file
@ -0,0 +1,48 @@
|
||||
package webcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
// ctxKeySession represents the type of value for the context key.
|
||||
type ctxKeySession int
|
||||
|
||||
// KeySession is used to store/retrieve a Session from a context.Context.
|
||||
const KeySession ctxKeySession = 1
|
||||
|
||||
// ContextWithSession appends a universal translator to a context.
|
||||
func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context {
|
||||
return context.WithValue(ctx, KeySession, session)
|
||||
}
|
||||
|
||||
// ContextSession returns the session from a context.
|
||||
func ContextSession(ctx context.Context) *sessions.Session {
|
||||
return ctx.Value(KeySession).(*sessions.Session)
|
||||
}
|
||||
|
||||
func ContextAccessToken(ctx context.Context) (string, bool) {
|
||||
session := ContextSession(ctx)
|
||||
|
||||
return SessionAccessToken(session)
|
||||
}
|
||||
|
||||
func SessionAccessToken(session *sessions.Session) (string, bool) {
|
||||
if sv, ok := session.Values["AccessToken"].(string); ok {
|
||||
return sv, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func SessionWithAccessToken(session *sessions.Session, accessToken string) *sessions.Session {
|
||||
|
||||
if accessToken != "" {
|
||||
session.Values["AccessToken"] = accessToken
|
||||
} else {
|
||||
delete(session.Values, "AccessToken")
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
41
internal/project-routes/project_routes.go
Normal file
41
internal/project-routes/project_routes.go
Normal file
@ -0,0 +1,41 @@
|
||||
package project_routes
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type ProjectRoutes struct {
|
||||
webAppUrl url.URL
|
||||
webApiUrl url.URL
|
||||
}
|
||||
|
||||
func New(apiBaseUrl, appBaseUrl string) (ProjectRoutes, error) {
|
||||
var r ProjectRoutes
|
||||
|
||||
apiUrl, err := url.Parse(apiBaseUrl)
|
||||
if err != nil {
|
||||
return r, errors.WithMessagef(err, "Failed to parse api base URL '%s'", apiBaseUrl)
|
||||
}
|
||||
r.webApiUrl = *apiUrl
|
||||
|
||||
appUrl, err := url.Parse(appBaseUrl)
|
||||
if err != nil {
|
||||
return r, errors.WithMessagef(err, "Failed to parse app base URL '%s'", appBaseUrl)
|
||||
}
|
||||
r.webAppUrl = *appUrl
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r ProjectRoutes) WebAppUrl(urlPath string) string {
|
||||
u := r.webAppUrl
|
||||
u.Path = urlPath
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (r ProjectRoutes) WebApiUrl(urlPath string) string {
|
||||
u := r.webApiUrl
|
||||
u.Path = urlPath
|
||||
return u.String()
|
||||
}
|
@ -2,6 +2,9 @@ package signup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"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/webcontext"
|
||||
@ -10,7 +13,6 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Signup performs the steps needed to create a new account, new user and then associate
|
||||
@ -36,12 +38,15 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
|
||||
return false
|
||||
}
|
||||
|
||||
fieldName := strings.Trim(fl.FieldName(), "{}")
|
||||
|
||||
var uniq bool
|
||||
switch fl.FieldName() {
|
||||
switch fieldName {
|
||||
case "Name", "name":
|
||||
uniq = uniqName
|
||||
case "Email", "email":
|
||||
uniq = uniqEmail
|
||||
|
||||
}
|
||||
|
||||
return uniq
|
||||
|
@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"database/sql"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
@ -291,6 +291,7 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
||||
|
||||
if expires.Seconds() > 0 {
|
||||
tkn.Expiry = now.Add(expires)
|
||||
tkn.TTL = expires
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
|
@ -110,6 +110,12 @@ type UserFindRequest struct {
|
||||
IncludedArchived bool `json:"included-archived" example:"false"`
|
||||
}
|
||||
|
||||
// AuthenticateRequest defines what information is required to authenticate a user.
|
||||
type AuthenticateRequest struct {
|
||||
Email string `json:"email" validate:"required,email" example:"gabi.may@geeksinthewoods.com"`
|
||||
Password string `json:"password" validate:"required" example:"NeverTellSecret"`
|
||||
}
|
||||
|
||||
// Token is the payload we deliver to users when they authenticate.
|
||||
type Token struct {
|
||||
// AccessToken is the token that authorizes and authenticates
|
||||
@ -123,7 +129,8 @@ type Token struct {
|
||||
// If zero, TokenSource implementations will reuse the same
|
||||
// token forever and RefreshToken or equivalent
|
||||
// mechanisms for that TokenSource will not be used.
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
TTL time.Duration `json:"ttl,omitempty"`
|
||||
// contains filtered or unexported fields
|
||||
claims auth.Claims `json:"-"`
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pborman/uuid"
|
||||
|
Reference in New Issue
Block a user