You've already forked golang-saas-starter-kit
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:
@@ -74,7 +74,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Add the token to the users session.
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -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)
|
||||
if err != nil {
|
||||
switch errors.Cause(err) {
|
||||
case account.ErrForbidden:
|
||||
case user.ErrForbidden:
|
||||
return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
|
||||
default:
|
||||
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.
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
if err != nil {
|
||||
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.
|
||||
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 == "" {
|
||||
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)
|
||||
|
||||
if sess.IsNew {
|
||||
@@ -134,7 +144,7 @@ func handleSessionToken(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
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 {
|
||||
return err
|
||||
@@ -149,7 +159,7 @@ func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
sess := webcontext.ContextSession(ctx)
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
err = handleSessionToken(ctx, w, r, token)
|
||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -6,7 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"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/user"
|
||||
"gopkg.in/gomail.v2"
|
||||
"html/template"
|
||||
"log"
|
||||
@@ -674,6 +676,23 @@ func main() {
|
||||
|
||||
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
|
||||
|
@@ -154,25 +154,52 @@
|
||||
<!-- Nav Item - User Information -->
|
||||
<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">
|
||||
<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>
|
||||
<!-- Dropdown - User Information -->
|
||||
<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>
|
||||
Profile
|
||||
My Profile
|
||||
</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>
|
||||
Settings
|
||||
</a>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||
Activity Log
|
||||
Support
|
||||
</a>
|
||||
|
||||
<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>
|
||||
Logout
|
||||
</a>
|
||||
|
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||
"github.com/dustin/go-humanize"
|
||||
@@ -97,3 +98,17 @@ func EnumValueTitle(v string) string {
|
||||
v = strings.Replace(v, "_", " ", -1)
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
@@ -3,9 +3,6 @@ package template_renderer
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -15,7 +12,10 @@ import (
|
||||
"reflect"
|
||||
"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"
|
||||
"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
|
||||
// basis.
|
||||
// append the global key/pairs
|
||||
renderData := r.globalViewData
|
||||
if renderData == nil {
|
||||
renderData = make(map[string]interface{})
|
||||
renderData := make(map[string]interface{}, len(r.globalViewData))
|
||||
for k, v := range r.globalViewData {
|
||||
renderData[k] = v
|
||||
}
|
||||
|
||||
// Add Request URL to render data
|
||||
|
@@ -15,6 +15,12 @@ const KeySession ctxKeySession = 1
|
||||
// KeyAccessToken is used to store the access token for the user in their session.
|
||||
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.
|
||||
func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context {
|
||||
return context.WithValue(ctx, KeySession, session)
|
||||
@@ -39,7 +45,23 @@ func SessionAccessToken(session *sessions.Session) (string, bool) {
|
||||
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 != "" {
|
||||
session.Values[KeyAccessToken] = accessToken
|
||||
@@ -47,5 +69,27 @@ func SessionWithAccessToken(session *sessions.Session, accessToken string) *sess
|
||||
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
|
||||
}
|
||||
|
||||
func SessionDestroy(session *sessions.Session) *sessions.Session {
|
||||
|
||||
delete(session.Values, KeyAccessToken)
|
||||
delete(session.Values, KeyUser)
|
||||
delete(session.Values, KeyAccount)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
|
@@ -287,6 +287,8 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
||||
AccessToken: tknStr,
|
||||
TokenType: "Bearer",
|
||||
claims: claims,
|
||||
UserID: claims.Subject,
|
||||
AccountID: claims.Audience,
|
||||
}
|
||||
|
||||
if expires.Seconds() > 0 {
|
||||
|
@@ -35,6 +35,7 @@ type UserResponse struct {
|
||||
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.
|
||||
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.
|
||||
@@ -52,6 +53,7 @@ func (m *User) Response(ctx context.Context) *UserResponse {
|
||||
Timezone: m.Timezone,
|
||||
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||
Gravatar: web.NewGravatarResponse(ctx, m.Email),
|
||||
}
|
||||
|
||||
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||
@@ -164,4 +166,8 @@ type Token struct {
|
||||
TTL time.Duration `json:"ttl,omitempty"`
|
||||
// contains filtered or unexported fields
|
||||
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"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user