Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for batch invite #325

Merged
merged 3 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,22 @@ userReqInvite.Tenants = []*descope.AssociatedTenant{
options := &descope.InviteOptions{InviteURL: "https://sub.domain.com"}
err := descopeClient.Management.User().Invite("[email protected]", userReqInvite, options)

// batch invite
options := &descope.InviteOptions{InviteURL: "https://sub.domain.com"}
batchUsers := []*descope.BatchUser{}
u1 := &descope.BatchUser{}
u1.LoginID = "one"
u1.Email = "[email protected]"
u1.Roles = []string{"one"}

u2 := &descope.BatchUser{}
u2.LoginID = "two"
u2.Email = "[email protected]"
u2.Roles = []string{"two"}

batchUsers = append(batchUsers, u1, u2)
err := descopeClient.Management.User().InviteBatch(batchUsers, options)

// Update will override all fields as is. Use carefully.
userReqUpdate := &descope.UserRequest{}
userReqUpdate.Email = "[email protected]"
Expand Down
6 changes: 6 additions & 0 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ var (
tenantLoadAll: "mgmt/tenant/all",
tenantSearchAll: "mgmt/tenant/search",
userCreate: "mgmt/user/create",
userCreateBatch: "mgmt/user/create/batch",
userUpdate: "mgmt/user/update",
userDelete: "mgmt/user/delete",
userDeleteAllTestUsers: "mgmt/user/test/delete/all",
Expand Down Expand Up @@ -205,6 +206,7 @@ type mgmtEndpoints struct {
tenantSearchAll string

userCreate string
userCreateBatch string
userUpdate string
userDelete string
userDeleteAllTestUsers string
Expand Down Expand Up @@ -437,6 +439,10 @@ func (e *endpoints) ManagementUserCreate() string {
return path.Join(e.version, e.mgmt.userCreate)
}

func (e *endpoints) ManagementUserCreateBatch() string {
return path.Join(e.version, e.mgmt.userCreateBatch)
}

func (e *endpoints) ManagementUserUpdate() string {
return path.Join(e.version, e.mgmt.userUpdate)
}
Expand Down
49 changes: 49 additions & 0 deletions descope/internal/mgmt/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ func (u *user) Invite(loginID string, user *descope.UserRequest, options *descop
return u.create(loginID, user.Email, user.Phone, user.Name, user.Picture, user.Roles, user.Tenants, true, false, user.CustomAttributes, user.VerifiedEmail, user.VerifiedPhone, options)
}

func (u *user) InviteBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) {
if users == nil {
users = []*descope.BatchUser{}
}
return u.createBatch(users, options)
}

func (u *user) create(loginID, email, phone, displayName, picture string, roles []string, tenants []*descope.AssociatedTenant, invite, test bool, customAttributes map[string]any, verifiedEmail *bool, verifiedPhone *bool, options *descope.InviteOptions) (*descope.UserResponse, error) {
if loginID == "" {
return nil, utils.NewInvalidArgumentError("loginID")
Expand All @@ -43,6 +50,15 @@ func (u *user) create(loginID, email, phone, displayName, picture string, roles
return unmarshalUserResponse(res)
}

func (u *user) createBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) {
req := makeCreateUsersBatchRequest(users, options)
res, err := u.client.DoPostRequest(api.Routes.ManagementUserCreateBatch(), req, nil, u.conf.ManagementKey)
if err != nil {
return nil, err
}
return unmarshalUserBatchResponse(res)
}

func (u *user) Update(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) {
if loginID == "" {
return nil, utils.NewInvalidArgumentError("loginID")
Expand Down Expand Up @@ -429,6 +445,30 @@ func makeCreateUserRequest(loginID, email, phone, displayName, picture string, r
return req
}

func makeCreateUsersBatchRequest(users []*descope.BatchUser, options *descope.InviteOptions) map[string]any {
var usersReq []map[string]any
for _, u := range users {
usersReq = append(usersReq, makeUpdateUserRequest(u.LoginID, u.Email, u.Phone, u.Name, u.Picture, u.Roles, u.Tenants, u.CustomAttributes, u.VerifiedEmail, u.VerifiedPhone))
}
req := map[string]any{
"users": usersReq,
}
if options != nil {
req["invite"] = true
if len(options.InviteURL) > 0 {
req["inviteUrl"] = options.InviteURL
}
if options.SendMail != nil {
req["sendMail"] = *options.SendMail
}
if options.SendSMS != nil {
req["sendSMS"] = *options.SendSMS
}
}

return req
}

func makeUpdateUserRequest(loginID, email, phone, displayName, picture string, roles []string, tenants []*descope.AssociatedTenant, customAttributes map[string]any, verifiedEmail *bool, verifiedPhone *bool) map[string]any {
res := map[string]any{
"loginId": loginID,
Expand Down Expand Up @@ -503,6 +543,15 @@ func unmarshalUserResponse(res *api.HTTPResponse) (*descope.UserResponse, error)
return ures.User, err
}

func unmarshalUserBatchResponse(res *api.HTTPResponse) (*descope.UsersBatchResponse, error) {
ures := &descope.UsersBatchResponse{}
err := utils.Unmarshal([]byte(res.BodyStr), ures)
if err != nil {
return nil, err
}
return ures, err
}

func unmarshalUserSearchAllResponse(res *api.HTTPResponse) ([]*descope.UserResponse, error) {
ures := struct {
Users []*descope.UserResponse
Expand Down
75 changes: 75 additions & 0 deletions descope/internal/mgmt/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,81 @@ func TestUserCreateSuccessWithOptions(t *testing.T) {
require.Equal(t, "[email protected]", res.Email)
}

func TestUsersInviteBatchSuccess(t *testing.T) {
response := map[string]any{
"createdUsers": []map[string]any{
{"email": "[email protected]"},
},
"failedUsers": []map[string]any{
{
"user": map[string]any{
"email": "[email protected]",
},
"failure": "some failure",
},
},
}
ca := map[string]any{"ak": "av"}

users := []*descope.BatchUser{}

u1 := &descope.BatchUser{}
u1.LoginID = "one"
u1.Email = "[email protected]"
u1.Roles = []string{"one"}
u1.CustomAttributes = ca

u2 := &descope.BatchUser{}
u2.LoginID = "two"
u2.Email = "[email protected]"
u2.Roles = []string{"two"}

users = append(users, u1, u2)

sendSMS := true

called := false
m := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) {
called = true
require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key")
req := map[string]any{}
require.NoError(t, helpers.ReadBody(r, &req))
assert.EqualValues(t, true, req["invite"])
assert.EqualValues(t, "https://some.domain.com", req["inviteUrl"])
assert.Nil(t, req["sendMail"])
assert.EqualValues(t, true, req["sendSMS"])
usersRes := req["users"].([]any)
userRes1 := usersRes[0].(map[string]any)
userRes2 := usersRes[1].(map[string]any)
require.Equal(t, u1.LoginID, userRes1["loginId"])
require.Equal(t, u1.Email, userRes1["email"])
assert.EqualValues(t, ca, userRes1["customAttributes"])
roleNames := userRes1["roleNames"].([]any)
require.Len(t, roleNames, 1)
require.Equal(t, u1.Roles[0], roleNames[0])

require.Equal(t, u2.LoginID, userRes2["loginId"])
require.Equal(t, u2.Email, userRes2["email"])
assert.Nil(t, userRes2["customAttributes"])
roleNames = userRes2["roleNames"].([]any)
require.Len(t, roleNames, 1)
require.Equal(t, u2.Roles[0], roleNames[0])
}, response))

res, err := m.User().InviteBatch(users, &descope.InviteOptions{
InviteURL: "https://some.domain.com",
SendSMS: &sendSMS,
})
require.True(t, called)
require.NoError(t, err)
require.NotNil(t, res)
require.Len(t, res.CreatedUsers, 1)
require.Len(t, res.FailedUsers, 1)
assert.EqualValues(t, u1.Email, res.CreatedUsers[0].Email)
assert.EqualValues(t, u2.Email, res.FailedUsers[0].User.Email)
assert.EqualValues(t, "some failure", res.FailedUsers[0].Failure)
}

func TestUserCreateTestUserSuccess(t *testing.T) {
response := map[string]any{
"user": map[string]any{
Expand Down
17 changes: 14 additions & 3 deletions descope/sdk/mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,28 @@ type User interface {
// Those users are not counted as part of the monthly active users
CreateTestUser(loginID string, user *descope.UserRequest) (*descope.UserResponse, error)

// Create a new user and invite them via an email message.
// Create a new user and invite via an email / text message.
//
// Functions exactly the same as the Create function with the additional invitation
// behavior. See the documentation above for the general creation behavior.
//
// IMPORTANT: Since the invitation is sent by email, make sure either
// the email is explicitly set, or the loginID itself is an email address.
// IMPORTANT: Since the invitation is sent by email / phone, make sure either
// the email / phone is explicitly set, or the loginID itself is an email address / phone number.
// You must configure the invitation URL in the Descope console prior to
// calling the method.
Invite(loginID string, user *descope.UserRequest, options *descope.InviteOptions) (*descope.UserResponse, error)

// Create users in batch and invite them via an email / text message.
//
// Functions exactly the same as the Create function with the additional invitation
// behavior. See the documentation above for the general creation behavior.
//
// IMPORTANT: Since the invitation is sent by email / phone, make sure either
// the email / phone is explicitly set, or the loginID itself is an email address / phone number.
// You must configure the invitation URL in the Descope console prior to
// calling the method.
InviteBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error)

// Update an existing user.
//
// The parameters follow the same convention as those for the Create function.
Expand Down
11 changes: 11 additions & 0 deletions descope/tests/mocks/mgmt/managementmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ type MockUser struct {
InviteResponse *descope.UserResponse
InviteError error

InviteBatchAssert func(users []*descope.BatchUser, options *descope.InviteOptions)
InviteBatchResponse *descope.UsersBatchResponse
InviteBatchError error

UpdateAssert func(loginID string, user *descope.UserRequest)
UpdateResponse *descope.UserResponse
UpdateError error
Expand Down Expand Up @@ -274,6 +278,13 @@ func (m *MockUser) Invite(loginID string, user *descope.UserRequest, options *de
return m.InviteResponse, m.InviteError
}

func (m *MockUser) InviteBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) {
if m.InviteBatchAssert != nil {
m.InviteBatchAssert(users, options)
}
return m.InviteBatchResponse, m.InviteBatchError
}

func (m *MockUser) Update(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) {
if m.UpdateAssert != nil {
m.UpdateAssert(loginID, user)
Expand Down
15 changes: 15 additions & 0 deletions descope/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ type UserRequest struct {
VerifiedPhone *bool `json:"verifiedPhone,omitempty"`
}

type BatchUser struct {
LoginID string `json:"loginId,omitempty"`
UserRequest `json:",inline"`
}

type UserResponse struct {
User `json:",inline"`
UserID string `json:"userId,omitempty"`
Expand All @@ -302,6 +307,16 @@ type UserResponse struct {
OAuth map[string]bool `json:"oauth,omitempty"`
}

type UsersFailedResponse struct {
Failure string `json:"failure,omitempty"`
User *UserResponse `json:"user,omitempty"`
}

type UsersBatchResponse struct {
CreatedUsers []*UserResponse `json:"createdUsers,omitempty"`
FailedUsers []*UsersFailedResponse `json:"failedUsers,omitempty"`
}

func (ur *UserResponse) GetCreatedTime() time.Time {
return time.Unix(int64(ur.CreatedTime), 0)
}
Expand Down