1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-08-08 22:36:41 +02:00

checkpoint

This commit is contained in:
Lee Brown
2019-08-03 16:35:57 -08:00
parent d4baa7a619
commit fad0801379
9 changed files with 148 additions and 25 deletions

View File

@ -74,7 +74,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, w, r, token) err = handleSessionToken(ctx, h.MasterDB, w, r, token)
if err != nil { if err != nil {
return err return err
} }

View File

@ -78,7 +78,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.Email, req.Password, sessionTTL, ctxValues.Now) token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.Email, req.Password, sessionTTL, ctxValues.Now)
if err != nil { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
case account.ErrForbidden: case user.ErrForbidden:
return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden)) return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
default: default:
if verr, ok := weberror.NewValidationError(ctx, err); ok { if verr, ok := weberror.NewValidationError(ctx, err); ok {
@ -91,7 +91,7 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, w, r, token) err = handleSessionToken(ctx, h.MasterDB, w, r, token)
if err != nil { if err != nil {
return err return err
} }
@ -117,11 +117,21 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
} }
// handleSessionToken persists the access token to the session for request authentication. // handleSessionToken persists the access token to the session for request authentication.
func handleSessionToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token user.Token) error { func handleSessionToken(ctx context.Context, db *sqlx.DB, w http.ResponseWriter, r *http.Request, token user.Token) error {
if token.AccessToken == "" { if token.AccessToken == "" {
return errors.New("accessToken is required.") return errors.New("accessToken is required.")
} }
usr, err := user.Read(ctx, auth.Claims{}, db, token.UserID, false )
if err != nil {
return err
}
acc, err := account.Read(ctx, auth.Claims{},db, token.AccountID, false )
if err != nil {
return err
}
sess := webcontext.ContextSession(ctx) sess := webcontext.ContextSession(ctx)
if sess.IsNew { if sess.IsNew {
@ -134,7 +144,7 @@ func handleSessionToken(ctx context.Context, w http.ResponseWriter, r *http.Requ
HttpOnly: false, HttpOnly: false,
} }
sess = webcontext.SessionWithAccessToken(sess, token.AccessToken) sess = webcontext.SessionInit(sess, token.AccessToken, usr.Response(ctx), acc.Response(ctx))
if err := sess.Save(r, w); err != nil { if err := sess.Save(r, w); err != nil {
return err return err
@ -149,7 +159,7 @@ func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Reques
sess := webcontext.ContextSession(ctx) sess := webcontext.ContextSession(ctx)
// Set the access token to empty to logout the user. // Set the access token to empty to logout the user.
sess = webcontext.SessionWithAccessToken(sess, "") sess = webcontext.SessionDestroy(sess)
if err := sess.Save(r, w); err != nil { if err := sess.Save(r, w); err != nil {
return err return err
@ -293,7 +303,7 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
} }
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, w, r, token) err = handleSessionToken(ctx, h.MasterDB, w, r, token)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,7 +6,9 @@ import (
"encoding/json" "encoding/json"
"expvar" "expvar"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
"html/template" "html/template"
"log" "log"
@ -674,6 +676,23 @@ func main() {
return fmt.Sprintf("%+v", err) return fmt.Sprintf("%+v", err)
}, },
"ContextUser": func(ctx context.Context) *user.UserResponse {
sess := webcontext.ContextSession(ctx)
v, _ := webcontext.SessionUser(sess)
if u, ok := v.(*user.UserResponse); ok {
return u
}
return nil
},
"ContextAccount": func(ctx context.Context) *account.AccountResponse {
sess := webcontext.ContextSession(ctx)
v, _ := webcontext.SessionAccount(sess)
if acc, ok := v.(*account.AccountResponse); ok {
return acc
}
return nil
},
} }
imgUrlFormatter := staticUrlFormatter imgUrlFormatter := staticUrlFormatter

View File

@ -154,25 +154,52 @@
<!-- Nav Item - User Information --> <!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow"> <li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Valerie Luna</span>
<img class="img-profile rounded-circle" src="https://source.unsplash.com/QAB-WJcbgJk/60x60"> {{ $user := ContextUser $._Ctx }}
{{ if $user }}
<span class="mr-2 d-none d-lg-inline text-gray-600 small">{{ $user.Name }}</span>
<img class="img-profile rounded-circle" src="{{ $user.Gravatar.Medium }}">
{{ else }}
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Space Cadet</span>
<img class="img-profile rounded-circle" src="src="{{ SiteAssetUrl "/assets/images/user-default.jpg"}}">
{{ end }}
</a> </a>
<!-- Dropdown - User Information --> <!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown"> <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
<a class="dropdown-item" href="#"> <a class="dropdown-item" href="/user">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i> <i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile My Profile
</a> </a>
<a class="dropdown-item" href="#">
{{ if HasRole $._Ctx "admin" }}
<a class="dropdown-item" href="/admin/account">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Account Settings
</a>
<a class="dropdown-item" href="/users">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Manage Users
</a>
<a class="dropdown-item" href="/users/invite">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Invite User
</a>
{{ else }}
<a class="dropdown-item" href="/account">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Account
</a>
{{ end }}
<a class="dropdown-item" href="/support">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i> <i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Settings Support
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
Activity Log
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal"> <a class="dropdown-item" href="/user/logout" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i> <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout Logout
</a> </a>

View File

@ -2,6 +2,7 @@ package web
import ( import (
"context" "context"
"crypto/md5"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
@ -97,3 +98,17 @@ func EnumValueTitle(v string) string {
v = strings.Replace(v, "_", " ", -1) v = strings.Replace(v, "_", " ", -1)
return strings.Title(v) return strings.Title(v)
} }
type GravatarResponse struct {
Small string `json:"small" example:"https://www.gravatar.com/avatar/xy7.jpg?s=30"`
Medium string `json:"medium" example:"https://www.gravatar.com/avatar/xy7.jpg?s=80"`
}
func NewGravatarResponse(ctx context.Context, email string) GravatarResponse {
u := fmt.Sprintf("https://www.gravatar.com/avatar/%x.jpg?s=", md5.Sum([]byte(strings.ToLower(email))))
return GravatarResponse{
Small: u+"30",
Medium: u+"80",
}
}

View File

@ -3,9 +3,6 @@ package template_renderer
import ( import (
"context" "context"
"fmt" "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" "html/template"
"math" "math"
"net/http" "net/http"
@ -15,7 +12,10 @@ import (
"reflect" "reflect"
"strings" "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"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
@ -307,9 +307,9 @@ func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, re
// Specific new data map for render to allow values to be overwritten on a request // Specific new data map for render to allow values to be overwritten on a request
// basis. // basis.
// append the global key/pairs // append the global key/pairs
renderData := r.globalViewData renderData := make(map[string]interface{}, len(r.globalViewData))
if renderData == nil { for k, v := range r.globalViewData {
renderData = make(map[string]interface{}) renderData[k] = v
} }
// Add Request URL to render data // Add Request URL to render data

View File

@ -15,6 +15,12 @@ const KeySession ctxKeySession = 1
// KeyAccessToken is used to store the access token for the user in their session. // KeyAccessToken is used to store the access token for the user in their session.
const KeyAccessToken = "AccessToken" const KeyAccessToken = "AccessToken"
// KeyUser is used to store the user in the session.
const KeyUser = "User"
// KeyAccount is used to store the account in the session.
const KeyAccount = "Account"
// ContextWithSession appends a universal translator to a context. // ContextWithSession appends a universal translator to a context.
func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context { func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context {
return context.WithValue(ctx, KeySession, session) return context.WithValue(ctx, KeySession, session)
@ -39,7 +45,23 @@ func SessionAccessToken(session *sessions.Session) (string, bool) {
return "", false return "", false
} }
func SessionWithAccessToken(session *sessions.Session, accessToken string) *sessions.Session { func SessionUser(session *sessions.Session) ( interface{}, bool) {
if sv, ok := session.Values[KeyUser]; ok && sv != nil {
return sv, true
}
return nil, false
}
func SessionAccount(session *sessions.Session) (interface{}, bool) {
if sv, ok := session.Values[KeyAccount]; ok && sv != nil {
return sv, true
}
return nil, false
}
func SessionInit(session *sessions.Session, accessToken string, usr interface{}, acc interface{}) *sessions.Session {
if accessToken != "" { if accessToken != "" {
session.Values[KeyAccessToken] = accessToken session.Values[KeyAccessToken] = accessToken
@ -47,5 +69,27 @@ func SessionWithAccessToken(session *sessions.Session, accessToken string) *sess
delete(session.Values, KeyAccessToken) delete(session.Values, KeyAccessToken)
} }
if usr != nil {
session.Values[KeyUser] = usr
} else {
delete(session.Values, KeyUser)
}
if acc != nil {
session.Values[KeyAccount] = acc
} else {
delete(session.Values, KeyAccount)
}
return session return session
} }
func SessionDestroy(session *sessions.Session) *sessions.Session {
delete(session.Values, KeyAccessToken)
delete(session.Values, KeyUser)
delete(session.Values, KeyAccount)
return session
}

View File

@ -287,6 +287,8 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
AccessToken: tknStr, AccessToken: tknStr,
TokenType: "Bearer", TokenType: "Bearer",
claims: claims, claims: claims,
UserID: claims.Subject,
AccountID: claims.Audience,
} }
if expires.Seconds() > 0 { if expires.Seconds() > 0 {

View File

@ -35,6 +35,7 @@ type UserResponse struct {
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display. CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display. UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display.
ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display. ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display.
Gravatar web.GravatarResponse `json:"gravatar"`
} }
// Response transforms User and UserResponse that is used for display. // Response transforms User and UserResponse that is used for display.
@ -52,6 +53,7 @@ func (m *User) Response(ctx context.Context) *UserResponse {
Timezone: m.Timezone, Timezone: m.Timezone,
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt), CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt), UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
Gravatar: web.NewGravatarResponse(ctx, m.Email),
} }
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() { if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
@ -164,4 +166,8 @@ type Token struct {
TTL time.Duration `json:"ttl,omitempty"` TTL time.Duration `json:"ttl,omitempty"`
// contains filtered or unexported fields // contains filtered or unexported fields
claims auth.Claims `json:"-"` claims auth.Claims `json:"-"`
// UserId is the ID of the user authenticated.
UserID string `json:"user_id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
// AccountID is the ID of the account for the user authenticated.
AccountID string `json:"account_id"example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
} }