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.
|
// 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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user