1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-15 00:15:15 +02:00

checkpoint for api handler tests

This commit is contained in:
Lee Brown
2019-06-26 01:16:57 -08:00
parent d6b6b605a4
commit b68bcf2c2c
16 changed files with 424 additions and 283 deletions

View File

@ -28,10 +28,10 @@ func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Reque
} }
// check redis // check redis
err = c.Redis.Ping().Err() //err = c.Redis.Ping().Err()
if err != nil { //if err != nil {
return errors.Wrap(err, "Redis failed") // return errors.Wrap(err, "Redis failed")
} //}
status := struct { status := struct {
Status string `json:"status"` Status string `json:"status"`

View File

@ -82,5 +82,7 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
// @Param data body web.TimeResponse false "Time Response" // @Param data body web.TimeResponse false "Time Response"
// @Param data body web.EnumResponse false "Enum Response" // @Param data body web.EnumResponse false "Enum Response"
// @Param data body web.EnumOption false "Enum Option" // @Param data body web.EnumOption false "Enum Option"
// @Param data body signup.SignupAccount false "SignupAccount"
// @Param data body signup.SignupUser false "SignupUser"
// To support nested types not parsed by swag. // To support nested types not parsed by swag.
func Types() {} func Types() {}

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -228,6 +229,29 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
} }
} }
if claims.Audience != "" {
uaReq := user_account.UserAccountCreateRequest{
UserID: resp.User.ID,
AccountID: resp.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_Admin},
//Status: Use default value
}
_, err = user_account.Create(ctx, claims, u.MasterDB, uaReq, v.Now)
if err != nil {
switch err {
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
_, ok := err.(validator.ValidationErrors)
if ok {
return web.NewRequestError(err, http.StatusBadRequest)
}
return errors.Wrapf(err, "User account: %+v", &req)
}
}
}
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated) return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
} }

View File

@ -1,5 +1,6 @@
package tests package tests
/*
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
@ -16,6 +17,7 @@ import (
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
// TestProjects is the entry point for the projects // TestProjects is the entry point for the projects
func TestProjects(t *testing.T) { func TestProjects(t *testing.T) {
defer tests.Recover(t) defer tests.Recover(t)
@ -447,3 +449,4 @@ func putProject204(t *testing.T, id string) {
} }
} }
} }
*/

View File

@ -1,8 +1,9 @@
package tests package tests
import ( import (
"crypto/rand" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"crypto/rsa" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
"github.com/pborman/uuid"
"net/http" "net/http"
"os" "os"
"testing" "testing"
@ -18,10 +19,20 @@ var a http.Handler
var test *tests.Test var test *tests.Test
// Information about the users we have created for testing. // Information about the users we have created for testing.
var adminAuthorization string type roleTest struct {
var adminID string Token user.Token
var userAuthorization string Claims auth.Claims
var userID string SignupRequest *signup.SignupRequest
SignupResponse *signup.SignupResponse
User *user.User
Account *account.Account
}
var roleTests map[string]roleTest
func init() {
roleTests = make(map[string]roleTest)
}
// TestMain is the entry point for testing. // TestMain is the entry point for testing.
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -32,66 +43,90 @@ func testMain(m *testing.M) int {
test = tests.New() test = tests.New()
defer test.TearDown() defer test.TearDown()
// Create RSA keys to enable authentication in our service. now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
kid := "4754d86b-7a6d-4df5-9c65-224741361492" authenticator, err := auth.NewAuthenticatorMemory(now)
kf := auth.NewSingleKeyFunc(kid, key.Public().(*rsa.PublicKey))
authenticator, err := auth.NewAuthenticator(key, kid, "RS256", kf)
if err != nil { if err != nil {
panic(err) panic(err)
} }
shutdown := make(chan os.Signal, 1) shutdown := make(chan os.Signal, 1)
a = handlers.API(shutdown, test.Log, test.MasterDB, authenticator) a = handlers.API(shutdown, test.Log, test.MasterDB, nil, authenticator)
// Create an admin user directly with our business logic. This creates an // Create a new account directly business logic. This creates an
// initial user that we will use for admin validated endpoints. // initial account and user that we will use for admin validated endpoints.
nu := user.NewUser{ signupReq := signup.SignupRequest{
Email: "admin@ardanlabs.com", Account: signup.SignupAccount{
Name: "Admin User", Name: uuid.NewRandom().String(),
Roles: []string{auth.RoleAdmin, auth.RoleUser}, Address1: "103 East Main St",
Password: "gophers", Address2: "Unit 546",
PasswordConfirm: "gophers", City: "Valdez",
Region: "AK",
Country: "USA",
Zipcode: "99686",
},
User: signup.SignupUser{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
} }
signup, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq, now)
admin, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now())
if err != nil {
panic(err)
}
adminID = admin.ID.Hex()
tkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password)
if err != nil { if err != nil {
panic(err) panic(err)
} }
adminAuthorization = "Bearer " + tkn.Token expires := time.Now().UTC().Sub(signup.User.CreatedAt) + time.Hour
adminTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, signupReq.User.Email, signupReq.User.Password, expires, now)
if err != nil {
panic(err)
}
adminClaims, err := authenticator.ParseClaims(adminTkn.AccessToken)
if err != nil {
panic(err)
}
roleTests[auth.RoleAdmin] = roleTest{
Token: adminTkn,
Claims: adminClaims,
SignupRequest: &signupReq,
SignupResponse: signup,
User: signup.User,
Account: signup.Account,
}
// Create a regular user to use when calling regular validated endpoints. // Create a regular user to use when calling regular validated endpoints.
nu = user.NewUser{ userReq := user.UserCreateRequest{
Email: "user@ardanlabs.com", Name: "Lucas Brown",
Name: "Regular User", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Roles: []string{auth.RoleUser}, Password: "akTechFr0n!ier",
Password: "concurrency", PasswordConfirm: "akTechFr0n!ier",
PasswordConfirm: "concurrency",
} }
usr, err := user.Create(tests.Context(), adminClaims, test.MasterDB, userReq, now)
usr, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now())
if err != nil {
panic(err)
}
userID = usr.ID.Hex()
tkn, err = user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password)
if err != nil { if err != nil {
panic(err) panic(err)
} }
userAuthorization = "Bearer " + tkn.Token userTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, usr.Email, userReq.Password, expires, now)
if err != nil {
panic(err)
}
userClaims, err := authenticator.ParseClaims(userTkn.AccessToken)
if err != nil {
panic(err)
}
roleTests[auth.RoleUser] = roleTest{
Token: userTkn,
Claims: userClaims,
SignupRequest: &signupReq,
SignupResponse: signup,
Account: signup.Account,
User: usr,
}
return m.Run() return m.Run()
} }

View File

@ -1,5 +1,6 @@
package tests package tests
/*
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
@ -574,3 +575,4 @@ func putUser403(t *testing.T, id string) {
} }
} }
} }
*/

View File

@ -1,15 +1,9 @@
package auth package auth
import ( import (
"fmt"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time" "time"
) )
@ -21,191 +15,58 @@ type Storage interface {
Current() *PrivateKey Current() *PrivateKey
} }
// StorageFile is a storage engine that stores private keys on the local file system. // StorageMemory is a storage engine that stores a single private key in memory.
type StorageFile struct { type StorageMemory struct {
// Local directory for storing private keys. privateKey *PrivateKey
localDir string
// Duration for keys to be valid.
keyExpiration time.Duration
// Map of keys by kid (version id).
keys map[string]*PrivateKey
// The current active key to be used.
curPrivateKey *PrivateKey
} }
// Keys returns a map of private keys by kID. // Keys returns a map of private keys by kID.
func (s *StorageFile) Keys() map[string]*PrivateKey { func (s *StorageMemory) Keys() map[string]*PrivateKey {
if s == nil || s.keys == nil { if s == nil || s.privateKey == nil {
return map[string]*PrivateKey{} return map[string]*PrivateKey{}
} }
return s.keys return map[string]*PrivateKey{
s.privateKey.keyID: s.privateKey,
}
} }
// Current returns the most recently generated private key. // Current returns the most recently generated private key.
func (s *StorageFile) Current() *PrivateKey { func (s *StorageMemory) Current() *PrivateKey {
if s == nil { if s == nil {
return nil return nil
} }
return s.curPrivateKey return s.privateKey
} }
// NewAuthenticatorFile is a help function that inits a new Authenticator // NewAuthenticatorMemory is a help function that inits a new Authenticator with a single key stored in memory.
// using the file storage. func NewAuthenticatorMemory(now time.Time) (*Authenticator, error) {
func NewAuthenticatorFile(localDir string, now time.Time, keyExpiration time.Duration) (*Authenticator, error) { storage, err := NewStorageMemory()
storage, err := NewStorageFile(localDir, now, keyExpiration)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewAuthenticator(storage, time.Now().UTC()) return NewAuthenticator(storage, now)
} }
// NewStorageFile implements the interface Storage to support persisting private keys // NewStorageMemory implements the interface Storage to store a single key in memory.
// to the local file system. func NewStorageMemory() (*StorageMemory, error) {
// It will error if:
func NewStorageFile(localDir string, now time.Time, keyExpiration time.Duration) (*StorageFile, error) {
if localDir == "" {
localDir = filepath.Join(os.TempDir(), "auth-private-keys")
}
if _, err := os.Stat(localDir); os.IsNotExist(err) { privateKey, err := KeyGen()
err = os.MkdirAll(localDir, os.ModePerm)
if err != nil {
return nil, errors.Wrapf(err, "failed to create storage directory %s", localDir)
}
}
storage := &StorageFile{
localDir: localDir,
keyExpiration: keyExpiration,
keys: make(map[string]*PrivateKey),
}
if now.IsZero() {
now = time.Now().UTC()
}
// Time threshold to stop loading keys, any key with a created date
// before this value will not be loaded.
var disabledCreatedDate time.Time
// Time threshold to create a new key. If a current key exists and the
// created date of the key is before this value, a new key will be created.
var activeCreatedDate time.Time
// If an expiration duration is included, convert to past time from now.
if keyExpiration.Seconds() != 0 {
// Ensure the expiration is a time in the past for comparison below.
if keyExpiration.Seconds() > 0 {
keyExpiration = keyExpiration * -1
}
// Stop loading keys when the created date exceeds two times the key expiration
disabledCreatedDate = now.UTC().Add(keyExpiration * 2)
// Time used to determine when a new key should be created.
activeCreatedDate = now.UTC().Add(keyExpiration)
}
// Values used to format filename.
filePrefix := "sassauth_"
fileExt := ".privatekey"
files, err := ioutil.ReadDir(localDir)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to list files in directory %s", localDir) return nil, errors.Wrap(err, "failed to generate new private key")
} }
// Map of keys stored by version id. version id is kid. pk, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
keyContents := make(map[string][]byte) if err != nil {
return nil, errors.Wrap(err, "parsing auth private key")
// The current key id if there is an active one.
var curKeyId string
// The max created data to determine the most recent key.
var lastCreatedDate time.Time
for _, f := range files {
if !strings.HasPrefix(f.Name(), filePrefix) || !strings.HasSuffix(f.Name(), fileExt) {
continue
}
// Extract the created timestamp and kID from the filename.
fname := strings.TrimSuffix(f.Name(), fileExt)
pts := strings.Split(fname, "_")
if len(pts) != 3 {
return nil, errors.Errorf("unable to parse filename %s", f.Name())
}
createdAt := pts[1]
kID := pts[2]
// Covert string timestamp to int.
createdAtSecs, err := strconv.Atoi(createdAt)
if err != nil {
return nil, errors.Wrapf(err, "failed parse timestamp from %s", f.Name())
}
ts := time.Unix(int64(createdAtSecs), 0)
// If the created time of the key is less than the disabled threshold, skip.
if !disabledCreatedDate.IsZero() && ts.UTC().Unix() < disabledCreatedDate.UTC().Unix() {
continue
}
filePath := filepath.Join(localDir, f.Name())
dat, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "failed read file %s", f.Name())
}
keyContents[kID] = dat
if lastCreatedDate.IsZero() || ts.UTC().Unix() > lastCreatedDate.UTC().Unix() {
curKeyId = kID
lastCreatedDate = ts.UTC()
}
} }
// storage := &StorageMemory{
if !activeCreatedDate.IsZero() && lastCreatedDate.UTC().Unix() < activeCreatedDate.UTC().Unix() { privateKey: &PrivateKey{
curKeyId = ""
}
// If there are no keys or the current key needs to be rotated, generate a new key.
if len(keyContents) == 0 || curKeyId == "" {
privateKey, err := KeyGen()
if err != nil {
return nil, errors.Wrap(err, "failed to generate new private key")
}
kID := uuid.NewRandom().String()
fname := fmt.Sprintf("%s%d_%s%s", filePrefix, now.UTC().Unix(), kID, fileExt)
filePath := filepath.Join(localDir, fname)
err = ioutil.WriteFile(filePath, privateKey, 0644)
if err != nil {
return nil, errors.Wrapf(err, "failed write file %s", filePath)
}
keyContents[curKeyId] = privateKey
}
// Loop through all the key bytes and load the private key.
for kid, key := range keyContents {
pk, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
return nil, errors.Wrap(err, "parsing auth private key")
}
storage.keys[kid] = &PrivateKey{
PrivateKey: pk, PrivateKey: pk,
keyID: kid, keyID: uuid.NewRandom().String(),
algorithm: algorithm, algorithm: algorithm,
} },
if kid == curKeyId {
storage.curPrivateKey = storage.keys[kid]
}
} }
return storage, nil return storage, nil

View File

@ -44,7 +44,7 @@ func NewAuthenticatorAws(awsSession *session.Session, awsSecretID string, now ti
return nil, err return nil, err
} }
return NewAuthenticator(storage, time.Now().UTC()) return NewAuthenticator(storage, now)
} }
// NewStorageAws implements the interface Storage to support persisting private keys // NewStorageAws implements the interface Storage to support persisting private keys

View File

@ -0,0 +1,204 @@
package auth
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pborman/uuid"
"github.com/pkg/errors"
)
// StorageFile is a storage engine that stores private keys on the local file system.
type StorageFile struct {
// Local directory for storing private keys.
localDir string
// Duration for keys to be valid.
keyExpiration time.Duration
// Map of keys by kid (version id).
keys map[string]*PrivateKey
// The current active key to be used.
curPrivateKey *PrivateKey
}
// Keys returns a map of private keys by kID.
func (s *StorageFile) Keys() map[string]*PrivateKey {
if s == nil || s.keys == nil {
return map[string]*PrivateKey{}
}
return s.keys
}
// Current returns the most recently generated private key.
func (s *StorageFile) Current() *PrivateKey {
if s == nil {
return nil
}
return s.curPrivateKey
}
// NewAuthenticatorFile is a help function that inits a new Authenticator
// using the file storage.
func NewAuthenticatorFile(localDir string, now time.Time, keyExpiration time.Duration) (*Authenticator, error) {
storage, err := NewStorageFile(localDir, now, keyExpiration)
if err != nil {
return nil, err
}
return NewAuthenticator(storage, now)
}
// NewStorageFile implements the interface Storage to support persisting private keys
// to the local file system.
func NewStorageFile(localDir string, now time.Time, keyExpiration time.Duration) (*StorageFile, error) {
if localDir == "" {
localDir = filepath.Join(os.TempDir(), "auth-private-keys")
}
if _, err := os.Stat(localDir); os.IsNotExist(err) {
err = os.MkdirAll(localDir, os.ModePerm)
if err != nil {
return nil, errors.Wrapf(err, "failed to create storage directory %s", localDir)
}
}
storage := &StorageFile{
localDir: localDir,
keyExpiration: keyExpiration,
keys: make(map[string]*PrivateKey),
}
if now.IsZero() {
now = time.Now().UTC()
}
// Time threshold to stop loading keys, any key with a created date
// before this value will not be loaded.
var disabledCreatedDate time.Time
// Time threshold to create a new key. If a current key exists and the
// created date of the key is before this value, a new key will be created.
var activeCreatedDate time.Time
// If an expiration duration is included, convert to past time from now.
if keyExpiration.Seconds() != 0 {
// Ensure the expiration is a time in the past for comparison below.
if keyExpiration.Seconds() > 0 {
keyExpiration = keyExpiration * -1
}
// Stop loading keys when the created date exceeds two times the key expiration
disabledCreatedDate = now.UTC().Add(keyExpiration * 2)
// Time used to determine when a new key should be created.
activeCreatedDate = now.UTC().Add(keyExpiration)
}
// Values used to format filename.
filePrefix := "sassauth_"
fileExt := ".privatekey"
files, err := ioutil.ReadDir(localDir)
if err != nil {
return nil, errors.Wrapf(err, "failed to list files in directory %s", localDir)
}
// Map of keys stored by version id. version id is kid.
keyContents := make(map[string][]byte)
// The current key id if there is an active one.
var curKeyId string
// The max created data to determine the most recent key.
var lastCreatedDate time.Time
for _, f := range files {
if !strings.HasPrefix(f.Name(), filePrefix) || !strings.HasSuffix(f.Name(), fileExt) {
continue
}
// Extract the created timestamp and kID from the filename.
fname := strings.TrimSuffix(f.Name(), fileExt)
pts := strings.Split(fname, "_")
if len(pts) != 3 {
return nil, errors.Errorf("unable to parse filename %s", f.Name())
}
createdAt := pts[1]
kID := pts[2]
// Covert string timestamp to int.
createdAtSecs, err := strconv.Atoi(createdAt)
if err != nil {
return nil, errors.Wrapf(err, "failed parse timestamp from %s", f.Name())
}
ts := time.Unix(int64(createdAtSecs), 0)
// If the created time of the key is less than the disabled threshold, skip.
if !disabledCreatedDate.IsZero() && ts.UTC().Unix() < disabledCreatedDate.UTC().Unix() {
continue
}
filePath := filepath.Join(localDir, f.Name())
dat, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "failed read file %s", f.Name())
}
keyContents[kID] = dat
if lastCreatedDate.IsZero() || ts.UTC().Unix() > lastCreatedDate.UTC().Unix() {
curKeyId = kID
lastCreatedDate = ts.UTC()
}
}
//
if !activeCreatedDate.IsZero() && lastCreatedDate.UTC().Unix() < activeCreatedDate.UTC().Unix() {
curKeyId = ""
}
// If there are no keys or the current key needs to be rotated, generate a new key.
if len(keyContents) == 0 || curKeyId == "" {
privateKey, err := KeyGen()
if err != nil {
return nil, errors.Wrap(err, "failed to generate new private key")
}
kID := uuid.NewRandom().String()
fname := fmt.Sprintf("%s%d_%s%s", filePrefix, now.UTC().Unix(), kID, fileExt)
filePath := filepath.Join(localDir, fname)
err = ioutil.WriteFile(filePath, privateKey, 0644)
if err != nil {
return nil, errors.Wrapf(err, "failed write file %s", filePath)
}
keyContents[curKeyId] = privateKey
}
// Loop through all the key bytes and load the private key.
for kid, key := range keyContents {
pk, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
return nil, errors.Wrap(err, "parsing auth private key")
}
storage.keys[kid] = &PrivateKey{
PrivateKey: pk,
keyID: kid,
algorithm: algorithm,
}
if kid == curKeyId {
storage.curPrivateKey = storage.keys[kid]
}
}
return storage, nil
}

View File

@ -7,25 +7,31 @@ import (
// SignupRequest contains information needed perform signup. // SignupRequest contains information needed perform signup.
type SignupRequest struct { type SignupRequest struct {
Account struct { Account SignupAccount `json:"account" validate:"required"` // Account details.
Name string `json:"name" validate:"required,unique" example:"Company {RANDOM_UUID}"` User SignupUser `json:"user" validate:"required"` // User details.
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
City string `json:"city" validate:"required" example:"Valdez"`
Region string `json:"region" validate:"required" example:"AK"`
Country string `json:"country" validate:"required" example:"USA"`
Zipcode string `json:"zipcode" validate:"required" example:"99686"`
Timezone *string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
} `json:"account" validate:"required"` // Account details.
User struct {
Name string `json:"name" validate:"required" example:"Gabi May"`
Email string `json:"email" validate:"required,email,unique" example:"{RANDOM_EMAIL}"`
Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
} `json:"user" validate:"required"` // User details.
} }
// SignupResponse contains information needed perform signup. // SignupAccount defined the details needed for account.
type SignupAccount struct {
Name string `json:"name" validate:"required,unique" example:"Company {RANDOM_UUID}"`
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
City string `json:"city" validate:"required" example:"Valdez"`
Region string `json:"region" validate:"required" example:"AK"`
Country string `json:"country" validate:"required" example:"USA"`
Zipcode string `json:"zipcode" validate:"required" example:"99686"`
Timezone *string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
}
// SignupUser defined the details needed for user.
type SignupUser struct {
Name string `json:"name" validate:"required" example:"Gabi May"`
Email string `json:"email" validate:"required,email,unique" example:"{RANDOM_EMAIL}"`
Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
}
// SignupResponse response signup with created account and user.
type SignupResponse struct { type SignupResponse struct {
Account *account.Account `json:"account"` Account *account.Account `json:"account"`
User *user.User `json:"user"` User *user.User `json:"user"`

View File

@ -96,7 +96,7 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
// Associate the created user with the new account. The first user for the account will // Associate the created user with the new account. The first user for the account will
// always have the role of admin. // always have the role of admin.
ua := user_account.CreateUserAccountRequest{ ua := user_account.UserAccountCreateRequest{
UserID: resp.User.ID, UserID: resp.User.ID,
AccountID: resp.Account.ID, AccountID: resp.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_Admin}, Roles: []user_account.UserAccountRole{user_account.UserAccountRole_Admin},

View File

@ -5,7 +5,6 @@ import (
"testing" "testing"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
@ -102,7 +101,7 @@ func TestSignupValidation(t *testing.T) {
func TestSignupFull(t *testing.T) { func TestSignupFull(t *testing.T) {
req := SignupRequest{ req := SignupRequest{
Account: account.AccountCreateRequest{ Account: SignupAccount{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -111,7 +110,7 @@ func TestSignupFull(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
User: user.UserCreateRequest{ User: SignupUser{
Name: "Lee Brown", Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",

View File

@ -295,6 +295,11 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
return tkn, nil return tkn, nil
} }
// AuthorizationHeader returns the header authorization value.
func (t Token) AuthorizationHeader() string {
return "Bearer " + t.AccessToken
}
// mockTokenGenerator is used for testing that Authenticate calls its provided // mockTokenGenerator is used for testing that Authenticate calls its provided
// token generator in a specific way. // token generator in a specific way.
type MockTokenGenerator struct { type MockTokenGenerator struct {

View File

@ -66,20 +66,20 @@ func (m *UserAccount) Response(ctx context.Context) *UserAccountResponse {
return r return r
} }
// CreateUserAccountRequest defines the information is needed to associate a user to an // UserAccountCreateRequest defines the information is needed to associate a user to an
// account. Users are global to the application and each users access can be managed // account. Users are global to the application and each users access can be managed
// on an account level. If a current entry exists in the database but is archived, // on an account level. If a current entry exists in the database but is archived,
// it will be un-archived. // it will be un-archived.
type CreateUserAccountRequest struct { type UserAccountCreateRequest struct {
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"` Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"` Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"`
} }
// UpdateUserAccountRequest defines the information needed to update the roles or the // UserAccountUpdateRequest defines the information needed to update the roles or the
// status for an existing user account. // status for an existing user account.
type UpdateUserAccountRequest struct { type UserAccountUpdateRequest struct {
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"` Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"`
@ -87,16 +87,16 @@ type UpdateUserAccountRequest struct {
unArchive bool `json:"-"` // Internal use only. unArchive bool `json:"-"` // Internal use only.
} }
// ArchiveUserAccountRequest defines the information needed to remove an existing account // UserAccountArchiveRequest defines the information needed to remove an existing account
// for a user. This will archive (soft-delete) the existing database entry. // for a user. This will archive (soft-delete) the existing database entry.
type ArchiveUserAccountRequest struct { type UserAccountArchiveRequest struct {
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
} }
// DeleteUserAccountRequest defines the information needed to delete an existing account // UserAccountDeleteRequest defines the information needed to delete an existing account
// for a user. This will hard delete the existing database entry. // for a user. This will hard delete the existing database entry.
type DeleteUserAccountRequest struct { type UserAccountDeleteRequest struct {
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
} }

View File

@ -201,7 +201,7 @@ func FindByUserID(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, user
} }
// Create a user account for a given user with specified roles. // Create a user account for a given user with specified roles.
func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req CreateUserAccountRequest, now time.Time) (*UserAccount, error) { func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAccountCreateRequest, now time.Time) (*UserAccount, error) {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Create") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Create")
defer span.Finish() defer span.Finish()
@ -243,7 +243,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Create
// If there is an existing entry, then update instead of insert. // If there is an existing entry, then update instead of insert.
var ua UserAccount var ua UserAccount
if len(existing) > 0 { if len(existing) > 0 {
upReq := UpdateUserAccountRequest{ upReq := UserAccountUpdateRequest{
UserID: req.UserID, UserID: req.UserID,
AccountID: req.AccountID, AccountID: req.AccountID,
Roles: &req.Roles, Roles: &req.Roles,
@ -315,7 +315,7 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i
} }
// Update replaces a user account in the database. // Update replaces a user account in the database.
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdateUserAccountRequest, now time.Time) error { func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAccountUpdateRequest, now time.Time) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Update") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Update")
defer span.Finish() defer span.Finish()
@ -387,7 +387,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Update
} }
// Archive soft deleted the user account from the database. // Archive soft deleted the user account from the database.
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req ArchiveUserAccountRequest, now time.Time) error { func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAccountArchiveRequest, now time.Time) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Archive") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Archive")
defer span.Finish() defer span.Finish()
@ -438,7 +438,7 @@ func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Archi
} }
// Delete removes a user account from the database. // Delete removes a user account from the database.
func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req DeleteUserAccountRequest) error { func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAccountDeleteRequest) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Delete") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.Delete")
defer span.Finish() defer span.Finish()

View File

@ -143,49 +143,49 @@ func TestCreateValidation(t *testing.T) {
var accountTests = []struct { var accountTests = []struct {
name string name string
req CreateUserAccountRequest req UserAccountCreateRequest
expected func(req CreateUserAccountRequest, res *UserAccount) *UserAccount expected func(req UserAccountCreateRequest, res *UserAccount) *UserAccount
error error error error
}{ }{
{"Required Fields", {"Required Fields",
CreateUserAccountRequest{}, UserAccountCreateRequest{},
func(req CreateUserAccountRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return nil return nil
}, },
errors.New("Key: 'CreateUserAccountRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" + errors.New("Key: 'UserAccountCreateRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" +
"Key: 'CreateUserAccountRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" + "Key: 'UserAccountCreateRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" +
"Key: 'CreateUserAccountRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"), "Key: 'UserAccountCreateRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"),
}, },
{"Valid Role", {"Valid Role",
CreateUserAccountRequest{ UserAccountCreateRequest{
UserID: uuid.NewRandom().String(), UserID: uuid.NewRandom().String(),
AccountID: uuid.NewRandom().String(), AccountID: uuid.NewRandom().String(),
Roles: []UserAccountRole{invalidRole}, Roles: []UserAccountRole{invalidRole},
}, },
func(req CreateUserAccountRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return nil return nil
}, },
errors.New("Key: 'CreateUserAccountRequest.Roles[0]' Error:Field validation for 'Roles[0]' failed on the 'oneof' tag"), errors.New("Key: 'UserAccountCreateRequest.Roles[0]' Error:Field validation for 'Roles[0]' failed on the 'oneof' tag"),
}, },
{"Valid Status", {"Valid Status",
CreateUserAccountRequest{ UserAccountCreateRequest{
UserID: uuid.NewRandom().String(), UserID: uuid.NewRandom().String(),
AccountID: uuid.NewRandom().String(), AccountID: uuid.NewRandom().String(),
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},
Status: &invalidStatus, Status: &invalidStatus,
}, },
func(req CreateUserAccountRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return nil return nil
}, },
errors.New("Key: 'CreateUserAccountRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"), errors.New("Key: 'UserAccountCreateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"),
}, },
{"Default Status", {"Default Status",
CreateUserAccountRequest{ UserAccountCreateRequest{
UserID: uuid.NewRandom().String(), UserID: uuid.NewRandom().String(),
AccountID: uuid.NewRandom().String(), AccountID: uuid.NewRandom().String(),
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},
}, },
func(req CreateUserAccountRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return &UserAccount{ return &UserAccount{
UserID: req.UserID, UserID: req.UserID,
AccountID: req.AccountID, AccountID: req.AccountID,
@ -288,7 +288,7 @@ func TestCreateExistingEntry(t *testing.T) {
t.Fatalf("\t%s\tMock account failed.", tests.Failed) t.Fatalf("\t%s\tMock account failed.", tests.Failed)
} }
req1 := CreateUserAccountRequest{ req1 := UserAccountCreateRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},
@ -301,7 +301,7 @@ func TestCreateExistingEntry(t *testing.T) {
t.Fatalf("\t%s\tCreate user account roles should match request. Diff:\n%s", tests.Failed, diff) t.Fatalf("\t%s\tCreate user account roles should match request. Diff:\n%s", tests.Failed, diff)
} }
req2 := CreateUserAccountRequest{ req2 := UserAccountCreateRequest{
UserID: req1.UserID, UserID: req1.UserID,
AccountID: req1.AccountID, AccountID: req1.AccountID,
Roles: []UserAccountRole{UserAccountRole_Admin}, Roles: []UserAccountRole{UserAccountRole_Admin},
@ -315,7 +315,7 @@ func TestCreateExistingEntry(t *testing.T) {
} }
// Now archive the user account to test trying to create a new entry for an archived entry // Now archive the user account to test trying to create a new entry for an archived entry
err = Archive(tests.Context(), auth.Claims{}, test.MasterDB, ArchiveUserAccountRequest{ err = Archive(tests.Context(), auth.Claims{}, test.MasterDB, UserAccountArchiveRequest{
UserID: req1.UserID, UserID: req1.UserID,
AccountID: req1.AccountID, AccountID: req1.AccountID,
}, now) }, now)
@ -334,7 +334,7 @@ func TestCreateExistingEntry(t *testing.T) {
} }
// Attempt to create the duplicate user account which should set archived_at back to nil // Attempt to create the duplicate user account which should set archived_at back to nil
req3 := CreateUserAccountRequest{ req3 := UserAccountCreateRequest{
UserID: req1.UserID, UserID: req1.UserID,
AccountID: req1.AccountID, AccountID: req1.AccountID,
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},
@ -368,32 +368,32 @@ func TestUpdateValidation(t *testing.T) {
var accountTests = []struct { var accountTests = []struct {
name string name string
req UpdateUserAccountRequest req UserAccountUpdateRequest
error error error error
}{ }{
{"Required Fields", {"Required Fields",
UpdateUserAccountRequest{}, UserAccountUpdateRequest{},
errors.New("Key: 'UpdateUserAccountRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" + errors.New("Key: 'UserAccountUpdateRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" +
"Key: 'UpdateUserAccountRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" + "Key: 'UserAccountUpdateRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" +
"Key: 'UpdateUserAccountRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"), "Key: 'UserAccountUpdateRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"),
}, },
{"Valid Role", {"Valid Role",
UpdateUserAccountRequest{ UserAccountUpdateRequest{
UserID: uuid.NewRandom().String(), UserID: uuid.NewRandom().String(),
AccountID: uuid.NewRandom().String(), AccountID: uuid.NewRandom().String(),
Roles: &UserAccountRoles{invalidRole}, Roles: &UserAccountRoles{invalidRole},
}, },
errors.New("Key: 'UpdateUserAccountRequest.Roles[0]' Error:Field validation for 'Roles[0]' failed on the 'oneof' tag"), errors.New("Key: 'UserAccountUpdateRequest.Roles[0]' Error:Field validation for 'Roles[0]' failed on the 'oneof' tag"),
}, },
{"Valid Status", {"Valid Status",
UpdateUserAccountRequest{ UserAccountUpdateRequest{
UserID: uuid.NewRandom().String(), UserID: uuid.NewRandom().String(),
AccountID: uuid.NewRandom().String(), AccountID: uuid.NewRandom().String(),
Roles: &UserAccountRoles{UserAccountRole_User}, Roles: &UserAccountRoles{UserAccountRole_User},
Status: &invalidStatus, Status: &invalidStatus,
}, },
errors.New("Key: 'UpdateUserAccountRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"), errors.New("Key: 'UserAccountUpdateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"),
}, },
} }
@ -550,7 +550,7 @@ func TestCrud(t *testing.T) {
} }
// Associate that with the user. // Associate that with the user.
createReq := CreateUserAccountRequest{ createReq := UserAccountCreateRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},
@ -576,7 +576,7 @@ func TestCrud(t *testing.T) {
} }
// Update the account. // Update the account.
updateReq := UpdateUserAccountRequest{ updateReq := UserAccountUpdateRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
Roles: &UserAccountRoles{UserAccountRole_Admin}, Roles: &UserAccountRoles{UserAccountRole_Admin},
@ -624,7 +624,7 @@ func TestCrud(t *testing.T) {
} }
// Archive (soft-delete) the user account. // Archive (soft-delete) the user account.
err = Archive(tests.Context(), tt.claims(userID, accountID), test.MasterDB, ArchiveUserAccountRequest{ err = Archive(tests.Context(), tt.claims(userID, accountID), test.MasterDB, UserAccountArchiveRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
}, now) }, now)
@ -667,7 +667,7 @@ func TestCrud(t *testing.T) {
t.Logf("\t%s\tArchive user account ok.", tests.Success) t.Logf("\t%s\tArchive user account ok.", tests.Success)
// Delete (hard-delete) the user account. // Delete (hard-delete) the user account.
err = Delete(tests.Context(), tt.claims(userID, accountID), test.MasterDB, DeleteUserAccountRequest{ err = Delete(tests.Context(), tt.claims(userID, accountID), test.MasterDB, UserAccountDeleteRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
}) })
@ -717,7 +717,7 @@ func TestFind(t *testing.T) {
} }
// Execute Create that will associate the user with the account. // Execute Create that will associate the user with the account.
ua, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, CreateUserAccountRequest{ ua, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, UserAccountCreateRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},