diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e4a016..30a26688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.18.0] - TODO + +### Added +- Account linking feature + +### Breaking changes +- The SDK now supports CDI 4.0 and removed support for older CDIs. +- `supertokens.DeleteUser` function now takes an extra boolean called `removeAllLinkedAccounts`. You can pass in `true` here for most cases. +- Added `supertokens.GetUser` function which can be used instead of recipe level getUser functions. +- Output type of `supertokens.GetUsersNewestFirst` and `supertokens.GetUsersOldestFirst` now return `supertokens.User` object instead of a generic string to interface{} map. +- Third party sign in up recipe function now also marks email as verified directly via a core call instead of relying on the email verification recipe. + ## [0.17.3] - 2023-12-12 - CI/CD changes diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 52dd1fb8..9ec2a0de 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of core-driver interfaces branch names that this core supports", "versions": [ - "3.0" + "4.0" ] } \ No newline at end of file diff --git a/frontendDriverInterfaceSupported.json b/frontendDriverInterfaceSupported.json index 0d1267d8..740998b1 100644 --- a/frontendDriverInterfaceSupported.json +++ b/frontendDriverInterfaceSupported.json @@ -1,6 +1,7 @@ { "_comment": "contains a list of frontend-driver interfaces branch names that this core supports", "versions": [ - "1.17" + "1.17", + "1.18" ] } \ No newline at end of file diff --git a/recipe/dashboard/api/userdetails/userDelete.go b/recipe/dashboard/api/userdetails/userDelete.go index f51ac744..c945b024 100644 --- a/recipe/dashboard/api/userdetails/userDelete.go +++ b/recipe/dashboard/api/userdetails/userDelete.go @@ -34,7 +34,8 @@ func UserDelete(apiInterface dashboardmodels.APIInterface, tenantId string, opti } } - deleteError := supertokens.DeleteUser(userId) + // TODO: pass in removeAllLinkedAccounts from the input to the API. + deleteError := supertokens.DeleteUser(userId, true) if deleteError != nil { return userDeleteResponse{}, deleteError diff --git a/recipe/dashboard/api/usersGet.go b/recipe/dashboard/api/usersGet.go index 9c20e5f1..bb3720b3 100644 --- a/recipe/dashboard/api/usersGet.go +++ b/recipe/dashboard/api/usersGet.go @@ -12,25 +12,20 @@ import ( ) type UsersGetResponse struct { - Status string `json:"status"` - NextPaginationToken *string `json:"nextPaginationToken,omitempty"` - Users []Users `json:"users"` + Status string `json:"status"` + NextPaginationToken *string `json:"nextPaginationToken,omitempty"` + Users []UserWithFirstAndLastName `json:"users"` } -type Users struct { - RecipeId string `json:"recipeId"` - User User `json:"user"` +type UserWithFirstAndLastName struct { + supertokens.User + firstName string + lastName string } -type User struct { - Id string `json:"id"` - TimeJoined float64 `json:"timeJoined"` - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - Email string `json:"email,omitempty"` - PhoneNumber string `json:"phoneNumber,omitempty"` - ThirdParty dashboardmodels.ThirdParty `json:"thirdParty,omitempty"` - TenantIds string `json:"tenantIds,omitempty"` +type UserPaginationResultWithFirstAndLastName struct { + Users []UserWithFirstAndLastName + NextPaginationToken *string } func UsersGet(apiImplementation dashboardmodels.APIInterface, tenantId string, options dashboardmodels.APIOptions, userContext supertokens.UserContext) (UsersGetResponse, error) { @@ -83,7 +78,8 @@ func UsersGet(apiImplementation dashboardmodels.APIInterface, tenantId string, o } if len(queryParamsObject) != 0 { - usersResponse, err = supertokens.GetUsersWithSearchParams(tenantId, timeJoinedOrder, paginationTokenPtr, &limit, nil, queryParamsObject) + // the oder here doesn't matter cause in search, we return all users anyway. + usersResponse, err = supertokens.GetUsersNewestFirst(tenantId, paginationTokenPtr, &limit, nil, queryParamsObject) } else if timeJoinedOrder == "ASC" { usersResponse, err = supertokens.GetUsersOldestFirst(tenantId, paginationTokenPtr, &limit, nil, nil) } else { @@ -93,12 +89,28 @@ func UsersGet(apiImplementation dashboardmodels.APIInterface, tenantId string, o return UsersGetResponse{}, err } + var userResponseWithFirstAndLastName UserPaginationResultWithFirstAndLastName = UserPaginationResultWithFirstAndLastName{} + + // copy userResponse into userResponseWithFirstAndLastName + userResponseWithFirstAndLastName.NextPaginationToken = usersResponse.NextPaginationToken + for _, userObj := range usersResponse.Users { + userResponseWithFirstAndLastName.Users = append(userResponseWithFirstAndLastName.Users, struct { + supertokens.User + firstName string + lastName string + }{ + User: userObj, + firstName: "", + lastName: "", + }) + } + _, err = usermetadata.GetRecipeInstanceOrThrowError() if err != nil { return UsersGetResponse{ Status: "OK", NextPaginationToken: usersResponse.NextPaginationToken, - Users: getUsersTypeFromPaginationResult(usersResponse), + Users: userResponseWithFirstAndLastName.Users, }, nil } @@ -109,7 +121,7 @@ func UsersGet(apiImplementation dashboardmodels.APIInterface, tenantId string, o var sem = make(chan int, batchSize) var errInBackground error - for i, userObj := range usersResponse.Users { + for i, userObj := range userResponseWithFirstAndLastName.Users { sem <- 1 if errInBackground != nil { @@ -117,18 +129,25 @@ func UsersGet(apiImplementation dashboardmodels.APIInterface, tenantId string, o } go func(i int, userObj struct { - RecipeId string `json:"recipeId"` - User map[string]interface{} `json:"user"` + supertokens.User + firstName string + lastName string }) { defer processingGroup.Done() - userMetadataResponse, err := usermetadata.GetUserMetadata(userObj.User["id"].(string), userContext) + userMetadataResponse, err := usermetadata.GetUserMetadata(userObj.ID, userContext) <-sem if err != nil { errInBackground = err return } - usersResponse.Users[i].User["firstName"] = userMetadataResponse["first_name"] - usersResponse.Users[i].User["lastName"] = userMetadataResponse["last_name"] + firstName, ok := userMetadataResponse["first_name"] + lastName, ok2 := userMetadataResponse["last_name"] + if ok { + userResponseWithFirstAndLastName.Users[i].firstName = firstName.(string) + } + if ok2 { + userResponseWithFirstAndLastName.Users[i].lastName = lastName.(string) + } }(i, userObj) } @@ -140,50 +159,7 @@ func UsersGet(apiImplementation dashboardmodels.APIInterface, tenantId string, o return UsersGetResponse{ Status: "OK", - NextPaginationToken: usersResponse.NextPaginationToken, - Users: getUsersTypeFromPaginationResult(usersResponse), + NextPaginationToken: userResponseWithFirstAndLastName.NextPaginationToken, + Users: userResponseWithFirstAndLastName.Users, }, nil } - -func getUsersTypeFromPaginationResult(usersResponse supertokens.UserPaginationResult) []Users { - users := []Users{} - for _, v := range usersResponse.Users { - user := User{ - Id: v.User["id"].(string), - TimeJoined: v.User["timeJoined"].(float64), - } - firstName := v.User["firstName"] - if firstName != nil { - user.FirstName = firstName.(string) - } - lastName := v.User["lastName"] - if lastName != nil { - user.LastName = lastName.(string) - } - - if v.RecipeId == "emailpassword" { - user.Email = v.User["email"].(string) - } else if v.RecipeId == "thirdparty" { - user.Email = v.User["email"].(string) - user.ThirdParty = dashboardmodels.ThirdParty{ - Id: v.User["thirdParty"].(map[string]interface{})["id"].(string), - UserId: v.User["thirdParty"].(map[string]interface{})["userId"].(string), - } - } else { - email := v.User["email"] - if email != nil { - user.Email = email.(string) - } - phoneNumber := v.User["phoneNumber"] - if phoneNumber != nil { - user.PhoneNumber = phoneNumber.(string) - } - } - - users = append(users, Users{ - RecipeId: v.RecipeId, - User: user, - }) - } - return users -} diff --git a/recipe/dashboard/userGet_test.go b/recipe/dashboard/userGet_test.go index 8bdd34a2..1624ae8c 100644 --- a/recipe/dashboard/userGet_test.go +++ b/recipe/dashboard/userGet_test.go @@ -2,6 +2,12 @@ package dashboard import ( "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "github.com/supertokens/supertokens-golang/recipe/dashboard/api" "github.com/supertokens/supertokens-golang/recipe/dashboard/api/userdetails" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" @@ -9,11 +15,6 @@ import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless/tplmodels" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" "github.com/stretchr/testify/assert" "github.com/supertokens/supertokens-golang/recipe/dashboard/dashboardmodels" @@ -145,7 +146,7 @@ func TestThatUserGetReturnsValidUserForThirdPartyUserWhenUsingThirdPartyPassword user := listResponse.Users[0].User - req, err = http.NewRequest(http.MethodGet, testServer.URL+"/auth/dashboard/api/user?userId="+user.Id+"&recipeId=thirdparty", strings.NewReader(`{}`)) + req, err = http.NewRequest(http.MethodGet, testServer.URL+"/auth/dashboard/api/user?userId="+user.ID+"&recipeId=thirdparty", strings.NewReader(`{}`)) req.Header.Set("Authorization", "Bearer testapikey") res, err = http.DefaultClient.Do(req) diff --git a/recipe/emailpassword/accountlinkingRecipeImplementation_test.go b/recipe/emailpassword/accountlinkingRecipeImplementation_test.go new file mode 100644 index 00000000..07fdc5be --- /dev/null +++ b/recipe/emailpassword/accountlinkingRecipeImplementation_test.go @@ -0,0 +1,2044 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package emailpassword + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" + "github.com/supertokens/supertokens-golang/recipe/emailverification" + "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" + "github.com/supertokens/supertokens-golang/test/unittesting" +) + +// we have this file here case we cannot put it in supertokens or unittesting +// package due to cyclic imports. + +func TestGetOldestUsersFirst(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + user1, err := SignUp("public", "test@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test1@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test2@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test3@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test4@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + + { + paginationResult, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) + if err != nil { + t.Error(err) + return + } + + email := "test@gmail.com" + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 5) + assert.True(t, paginationResult.Users[0].LoginMethods[0].HasSameEmailAs(&email)) + assert.Equal(t, paginationResult.Users[0].ID, user1.OK.User.ID) + assert.Equal(t, paginationResult.Users[0].ID, paginationResult.Users[0].LoginMethods[0].RecipeUserID.GetAsString()) + } + + { + limit := 1 + paginationResult, err := supertokens.GetUsersOldestFirst("public", nil, &limit, nil, nil) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.Equal(t, paginationResult.Users[0].Emails[0], "test@gmail.com") + + paginationResult, err = supertokens.GetUsersOldestFirst("public", paginationResult.NextPaginationToken, &limit, nil, nil) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.Equal(t, paginationResult.Users[0].Emails[0], "test1@gmail.com") + + limit = 5 + paginationResult, err = supertokens.GetUsersOldestFirst("public", paginationResult.NextPaginationToken, &limit, nil, nil) + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 3) + assert.Equal(t, paginationResult.Users[0].Emails[0], "test2@gmail.com") + } + + { + paginationToken := "invalid" + _, err := supertokens.GetUsersOldestFirst("public", &paginationToken, nil, nil, nil) + if err != nil { + assert.Contains(t, err.Error(), "invalid pagination token") + } else { + assert.Fail(t, "pagination token invalid should fail") + } + } +} + +func TestGetOldestUsersFirstTakesIntoAccountTenantId(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + enabled := true + + multitenancy.CreateOrUpdateTenant("t1", multitenancymodels.TenantConfig{ + EmailPasswordEnabled: &enabled, + }) + + user1, err := SignUp("t1", "test@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test1@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + + { + paginationResult, err := supertokens.GetUsersOldestFirst("t1", nil, nil, nil, nil) + if err != nil { + t.Error(err) + return + } + + email := "test@gmail.com" + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.True(t, paginationResult.Users[0].LoginMethods[0].HasSameEmailAs(&email)) + assert.Equal(t, paginationResult.Users[0].ID, user1.OK.User.ID) + assert.Equal(t, paginationResult.Users[0].ID, paginationResult.Users[0].LoginMethods[0].RecipeUserID.GetAsString()) + } + + { + paginationResult, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) + if err != nil { + t.Error(err) + return + } + + email := "test1@gmail.com" + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.True(t, paginationResult.Users[0].LoginMethods[0].HasSameEmailAs(&email)) + } +} + +func TestGetNewestUsersFirst(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + _, err := SignUp("public", "test@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test1@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test2@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test3@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + user5, err := SignUp("public", "test4@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + + { + paginationResult, err := supertokens.GetUsersNewestFirst("public", nil, nil, nil, nil) + if err != nil { + t.Error(err) + return + } + + email := "test4@gmail.com" + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 5) + assert.True(t, paginationResult.Users[0].LoginMethods[0].HasSameEmailAs(&email)) + assert.Equal(t, paginationResult.Users[0].ID, user5.OK.User.ID) + assert.Equal(t, paginationResult.Users[0].ID, paginationResult.Users[0].LoginMethods[0].RecipeUserID.GetAsString()) + } + + { + limit := 1 + paginationResult, err := supertokens.GetUsersNewestFirst("public", nil, &limit, nil, nil) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.Equal(t, paginationResult.Users[0].Emails[0], "test4@gmail.com") + + paginationResult, err = supertokens.GetUsersNewestFirst("public", paginationResult.NextPaginationToken, &limit, nil, nil) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.Equal(t, paginationResult.Users[0].Emails[0], "test3@gmail.com") + + limit = 5 + paginationResult, err = supertokens.GetUsersNewestFirst("public", paginationResult.NextPaginationToken, &limit, nil, nil) + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 3) + assert.Equal(t, paginationResult.Users[0].Emails[0], "test2@gmail.com") + } + + { + paginationToken := "invalid" + _, err := supertokens.GetUsersNewestFirst("public", &paginationToken, nil, nil, nil) + if err != nil { + assert.Contains(t, err.Error(), "invalid pagination token") + } else { + assert.Fail(t, "pagination token invalid should fail") + } + } +} + +func TestGetOldestUsersFirstWithSearchParams(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + _, err := SignUp("public", "test@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test1@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test2@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test3@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "john@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + + { + paginationResult, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, map[string]string{ + "email": "doe", + }) + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 0) + } + + { + paginationResult, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, map[string]string{ + "email": "john", + }) + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.Equal(t, paginationResult.Users[0].Emails[0], "john@gmail.com") + assert.Len(t, paginationResult.Users[0].LoginMethods, 1) + assert.Len(t, paginationResult.Users[0].Emails, 1) + assert.Len(t, paginationResult.Users[0].PhoneNumbers, 0) + assert.Len(t, paginationResult.Users[0].ThirdParty, 0) + } +} + +func TestGetNewestUsersFirstWithSearchParams(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + _, err := SignUp("public", "test@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test1@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test2@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "test3@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + _, err = SignUp("public", "john@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + + { + paginationResult, err := supertokens.GetUsersNewestFirst("public", nil, nil, nil, map[string]string{ + "email": "doe", + }) + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 0) + } + + { + paginationResult, err := supertokens.GetUsersNewestFirst("public", nil, nil, nil, map[string]string{ + "email": "john", + }) + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, paginationResult.NextPaginationToken) + assert.Len(t, paginationResult.Users, 1) + assert.Equal(t, paginationResult.Users[0].Emails[0], "john@gmail.com") + assert.Len(t, paginationResult.Users[0].LoginMethods, 1) + assert.Len(t, paginationResult.Users[0].Emails, 1) + assert.Len(t, paginationResult.Users[0].PhoneNumbers, 0) + assert.Len(t, paginationResult.Users[0].ThirdParty, 0) + } +} + +func TestGetUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + ogUser, err := SignUp("public", "test@gmail.com", "testPass123") + if err != nil { + t.Error(err) + return + } + + user, err := supertokens.GetUser(ogUser.OK.User.ID) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, user.ID, ogUser.OK.User.ID) + assert.Equal(t, user.Emails[0], "test@gmail.com") + assert.Len(t, user.LoginMethods, 1) + assert.Len(t, user.Emails, 1) + assert.Len(t, user.PhoneNumbers, 0) + assert.Len(t, user.ThirdParty, 0) + email := "test@gmail.com" + assert.True(t, user.LoginMethods[0].HasSameEmailAs(&email)) + assert.Equal(t, user.ID, user.LoginMethods[0].RecipeUserID.GetAsString()) + assert.Equal(t, supertokens.EmailPasswordRID, user.LoginMethods[0].RecipeID) + + user, err = supertokens.GetUser("random") + if err != nil { + t.Error(err) + return + } + + assert.Nil(t, user) +} + +func TestMakePrimaryUserSuccess(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + + assert.False(t, user1.IsPrimaryUser) + + response, err := supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.True(t, response.OK.User.IsPrimaryUser) + assert.False(t, response.OK.WasAlreadyAPrimaryUser) + + assert.Equal(t, user1.ID, response.OK.User.ID) + assert.Equal(t, user1.Emails[0], response.OK.User.Emails[0]) + assert.Len(t, response.OK.User.LoginMethods, 1) + + refetchedUser, err := supertokens.GetUser(user1.ID) + if err != nil { + t.Error(err) + return + } + assert.Equal(t, *refetchedUser, response.OK.User) +} + +func TestMakePrimaryUserSuccessAlreadyPrimaryUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + + assert.False(t, user1.IsPrimaryUser) + + response, err := supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.True(t, response.OK.User.IsPrimaryUser) + assert.False(t, response.OK.WasAlreadyAPrimaryUser) + + response2, err := supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.True(t, response2.OK.User.IsPrimaryUser) + assert.True(t, response2.OK.WasAlreadyAPrimaryUser) + assert.Equal(t, response2.OK.User.ID, response.OK.User.ID) +} + +func TestCanMakePrimaryUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + + assert.False(t, user1.IsPrimaryUser) + + response, err := supertokens.CanCreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + + assert.False(t, response.OK.WasAlreadyAPrimaryUser) + + _, err = supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + + response, err = supertokens.CanCreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + + assert.True(t, response.OK.WasAlreadyAPrimaryUser) +} + +func TestMakePrimaryFailCauseAlreadyLinkedToAnotherAccount(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + + assert.False(t, user2.IsPrimaryUser) + + _, err = supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + _, err = supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + + canCreatePrimaryUserResponse, err := supertokens.CanCreatePrimaryUser(user2.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, canCreatePrimaryUserResponse.OK) + assert.Equal(t, canCreatePrimaryUserResponse.RecipeUserIdAlreadyLinkedWithPrimaryUserIdError.PrimaryUserId, user1.ID) + + createPrimaryUserResponse, err := supertokens.CreatePrimaryUser(user2.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, createPrimaryUserResponse.OK) + assert.Equal(t, createPrimaryUserResponse.RecipeUserIdAlreadyLinkedWithPrimaryUserIdError.PrimaryUserId, user1.ID) +} + +func TestMakePrimaryFailCauseAccountInfoAlreadyAssociatedWithAnotherPrimaryUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + epuser1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, epuser1.IsPrimaryUser) + + tpuser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + + tpUser1 := convertTpUserToSuperTokensUser(tpuser.OK.User) + + _, err = supertokens.CreatePrimaryUser(tpUser1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + + canCreatePrimaryUserResult, err := supertokens.CanCreatePrimaryUser(epuser1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, canCreatePrimaryUserResult.OK) + assert.Equal(t, canCreatePrimaryUserResult.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError.PrimaryUserId, tpUser1.ID) + + createPrimaryUserResponse, err := supertokens.CreatePrimaryUser(epuser1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, createPrimaryUserResponse.OK) + assert.Equal(t, createPrimaryUserResponse.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError.PrimaryUserId, tpUser1.ID) +} + +func TestLinkAccountsSuccess(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + var primaryUserInCallback supertokens.User + var newAccountInfoInCallback supertokens.RecipeLevelUser + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + supertokens.InitAccountLinking(&supertokens.AccountLinkingTypeInput{ + OnAccountLinked: func(user supertokens.User, newAccountUser supertokens.RecipeLevelUser, userContext supertokens.UserContext) error { + primaryUserInCallback = user + newAccountInfoInCallback = newAccountUser + return nil + }, + }), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + assert.False(t, user2.IsPrimaryUser) + + // we create a new session to check that the session has not been revoked + // when we link accounts, cause these users are already linked. + session.CreateNewSessionWithoutRequestResponse("public", user2.ID, nil, nil, nil) + sessions, err := session.GetAllSessionHandlesForUser(user2.ID, nil) + if err != nil { + t.Error(err) + return + } + assert.Len(t, sessions, 1) + + { + linkAccountResponse, err := supertokens.CanLinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + } + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + linkedUser, err := supertokens.GetUser(user1.ID) + if err != nil { + t.Error(err) + return + } + assert.Equal(t, *linkedUser, primaryUserInCallback) + assert.Equal(t, *linkedUser, linkAccountResponse.OK.User) + + assert.Equal(t, newAccountInfoInCallback.RecipeID, supertokens.EmailPasswordRID) + assert.Equal(t, *newAccountInfoInCallback.Email, "test2@gmail.com") + sessions, err = session.GetAllSessionHandlesForUser(user2.LoginMethods[0].RecipeUserID.GetAsString(), nil) + if err != nil { + t.Error(err) + return + } + assert.Len(t, sessions, 0) +} + +func TestLinkAccountsSuccessAlreadyLinked(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + supertokens.InitAccountLinking(&supertokens.AccountLinkingTypeInput{}), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + assert.False(t, user2.IsPrimaryUser) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + { + canLinkAccountResponse, err := supertokens.CanLinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.True(t, canLinkAccountResponse.OK.AccountsAlreadyLinked) + } + + linkAccountResponse, err = supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.True(t, linkAccountResponse.OK.AccountsAlreadyLinked) +} + +func TestLinkAccountsFailureAlreadyLinkedWithAnotherPrimaryUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + callbackCalled := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + supertokens.InitAccountLinking(&supertokens.AccountLinkingTypeInput{ + OnAccountLinked: func(user supertokens.User, newAccountUser supertokens.RecipeLevelUser, userContext supertokens.UserContext) error { + callbackCalled = true + return nil + }, + }), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + assert.False(t, user2.IsPrimaryUser) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + assert.True(t, callbackCalled) + + callbackCalled = false + + epuser3, err := SignUp("public", "test3@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user3 := convertEpUserToSuperTokensUser(epuser3.OK.User) + assert.False(t, user3.IsPrimaryUser) + supertokens.CreatePrimaryUser(user3.LoginMethods[0].RecipeUserID) + + { + canLinkAccountResponse, err := supertokens.CanLinkAccounts(user2.LoginMethods[0].RecipeUserID, user3.ID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, canLinkAccountResponse.OK) + assert.NotNil(t, canLinkAccountResponse.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError) + assert.Equal(t, canLinkAccountResponse.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError.PrimaryUserId, user1.ID) + } + + linkAccountResponse, err = supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user3.ID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, linkAccountResponse.OK) + assert.NotNil(t, linkAccountResponse.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError) + assert.Equal(t, linkAccountResponse.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError.PrimaryUserId, user1.ID) + + assert.False(t, callbackCalled) +} + +func TestLinkAccountsFailureInputUserIdNotAPrimaryUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + supertokens.InitAccountLinking(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + assert.False(t, user2.IsPrimaryUser) + + { + linkAccountResponse, err := supertokens.CanLinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, linkAccountResponse.OK) + assert.NotNil(t, linkAccountResponse.InputUserIsNotAPrimaryUserError) + } + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.Nil(t, linkAccountResponse.OK) + assert.NotNil(t, linkAccountResponse.InputUserIsNotAPrimaryUserError) +} + +func TestLinkAccountFailureAccountInfoAlreadyAssociatedWithAnotherPrimaryUser(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + epuser1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, epuser1.IsPrimaryUser) + supertokens.CreatePrimaryUser(epuser1.LoginMethods[0].RecipeUserID) + + tpuser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + + tpUser1 := convertTpUserToSuperTokensUser(tpuser.OK.User) + + epuser, err = SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + epuser2 := convertEpUserToSuperTokensUser(epuser.OK.User) + supertokens.CreatePrimaryUser(epuser2.LoginMethods[0].RecipeUserID) + + { + linkAccountResponse, err := supertokens.CanLinkAccounts(tpUser1.LoginMethods[0].RecipeUserID, epuser2.ID) + if err != nil { + t.Error(err) + return + } + assert.NotNil(t, linkAccountResponse.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError) + assert.Equal(t, linkAccountResponse.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError.PrimaryUserId, epuser1.ID) + } + + linkAccountResponse, err := supertokens.LinkAccounts(tpUser1.LoginMethods[0].RecipeUserID, epuser2.ID) + if err != nil { + t.Error(err) + return + } + assert.NotNil(t, linkAccountResponse.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError) + assert.Equal(t, linkAccountResponse.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError.PrimaryUserId, epuser1.ID) + +} + +func TestUnlinkAccountsSuccess(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + supertokens.InitAccountLinking(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + assert.False(t, user2.IsPrimaryUser) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + session.CreateNewSessionWithoutRequestResponse("public", user2.ID, nil, nil, nil) + sessions, err := session.GetAllSessionHandlesForUser(user2.ID, nil) + if err != nil { + t.Error(err) + return + } + assert.Len(t, sessions, 1) + + unlinkResponse, err := supertokens.UnlinkAccounts(user2.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.False(t, unlinkResponse.WasRecipeUserDeleted) + assert.True(t, unlinkResponse.WasLinked) + + primaryUser, err := supertokens.GetUser(user1.ID) + if err != nil { + t.Error(err) + return + } + assert.Len(t, primaryUser.LoginMethods, 1) + assert.True(t, primaryUser.IsPrimaryUser) + + recipeUser, err := supertokens.GetUser(user2.ID) + if err != nil { + t.Error(err) + return + } + assert.Len(t, recipeUser.LoginMethods, 1) + assert.False(t, recipeUser.IsPrimaryUser) + + sessions, err = session.GetAllSessionHandlesForUser(user2.LoginMethods[0].RecipeUserID.GetAsString(), nil) + if err != nil { + t.Error(err) + return + } + assert.Len(t, sessions, 0) +} + +func TestUnlinkAccountsWithDeleteUserSuccess(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + supertokens.InitAccountLinking(nil), + }, + }) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user1 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + epuser2, err := SignUp("public", "test2@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser2.OK.User) + assert.False(t, user2.IsPrimaryUser) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + unlinkResponse, err := supertokens.UnlinkAccounts(user1.LoginMethods[0].RecipeUserID) + if err != nil { + t.Error(err) + return + } + assert.True(t, unlinkResponse.WasRecipeUserDeleted) + assert.True(t, unlinkResponse.WasLinked) +} + +func TestDeleteUser(t *testing.T) { + telemetry := false + configValue := supertokens.TypeInput{ + Telemetry: &telemetry, + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + APIDomain: "api.supertokens.io", + AppName: "SuperTokens", + WebsiteDomain: "supertokens.io", + }, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + } + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + err := supertokens.Init(configValue) + if err != nil { + t.Error(err.Error()) + } + + res, err := SignUp("public", "test@example.com", "1234abcd") + if err != nil { + t.Error(err.Error()) + } + reponseBeforeDeletingUser, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) + if err != nil { + t.Error(err.Error()) + } + assert.Equal(t, 1, len(reponseBeforeDeletingUser.Users)) + err = supertokens.DeleteUser(res.OK.User.ID, true) + if err != nil { + t.Error(err.Error()) + } + responseAfterDeletingUser, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) + if err != nil { + t.Error(err.Error()) + } + assert.Equal(t, 0, len(responseAfterDeletingUser.Users)) +} + +func TestLinkAccountCausesNewAccountsEmailToBeVerifiedIfSameEmail(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + tpUser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + user1 := convertTpUserToSuperTokensUser(tpUser.OK.User) + + token, err := emailverification.CreateEmailVerificationToken("public", user1.ID, nil) + if err != nil { + t.Error(err) + return + } + + _, err = emailverification.VerifyEmailUsingToken("public", token.OK.Token) + if err != nil { + t.Error(err) + return + } + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + isEmailVerified, err := emailverification.IsEmailVerified(user2.LoginMethods[0].RecipeUserID.GetAsString(), nil) + if err != nil { + t.Error(err) + return + } + assert.True(t, isEmailVerified) +} + +func TestLinkAccountDoesNotCausePrimaryAccountsEmailToBeVerifiedIfSameEmail(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + tpUser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + user1 := convertTpUserToSuperTokensUser(tpUser.OK.User) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + + token, err := emailverification.CreateEmailVerificationToken("public", user2.ID, nil) + if err != nil { + t.Error(err) + return + } + + _, err = emailverification.VerifyEmailUsingToken("public", token.OK.Token) + if err != nil { + t.Error(err) + return + } + + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + isEmailVerified, err := emailverification.IsEmailVerified(user1.LoginMethods[0].RecipeUserID.GetAsString(), nil) + if err != nil { + t.Error(err) + return + } + assert.False(t, isEmailVerified) +} + +func TestLinkAccountDoesNotCauseNewAccountsEmailToBeVerifiedIfDifferentEmail(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + tpUser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test1@gmail.com") + if err != nil { + t.Error(err) + return + } + user1 := convertTpUserToSuperTokensUser(tpUser.OK.User) + + token, err := emailverification.CreateEmailVerificationToken("public", user1.ID, nil) + if err != nil { + t.Error(err) + return + } + + _, err = emailverification.VerifyEmailUsingToken("public", token.OK.Token) + if err != nil { + t.Error(err) + return + } + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + isEmailVerified, err := emailverification.IsEmailVerified(user2.LoginMethods[0].RecipeUserID.GetAsString(), nil) + if err != nil { + t.Error(err) + return + } + assert.False(t, isEmailVerified) +} + +func TestLinkAccountDoesNotCauseNewAccountsEmailToBeVerifiedIfSameEmailButPrimaryIsNotVerified(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + tpUser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + user1 := convertTpUserToSuperTokensUser(tpUser.OK.User) + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + user2 := convertEpUserToSuperTokensUser(epuser.OK.User) + assert.False(t, user1.IsPrimaryUser) + + supertokens.CreatePrimaryUser(user1.LoginMethods[0].RecipeUserID) + + linkAccountResponse, err := supertokens.LinkAccounts(user2.LoginMethods[0].RecipeUserID, user1.ID) + if err != nil { + t.Error(err) + return + } + assert.False(t, linkAccountResponse.OK.AccountsAlreadyLinked) + + isEmailVerified, err := emailverification.IsEmailVerified(user2.LoginMethods[0].RecipeUserID.GetAsString(), nil) + if err != nil { + t.Error(err) + return + } + assert.False(t, isEmailVerified) +} + +func TestListUsersByAccountInfo(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + tpuser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + email := "test@gmail.com" + users, err := supertokens.ListUsersByAccountInfo("public", supertokens.AccountInfo{ + Email: &email, + }, false) + if err != nil { + t.Error(err) + return + } + assert.Len(t, users, 2) + + assert.Equal(t, users[0].ID, tpuser.OK.User.ID) + assert.Equal(t, users[1].ID, epuser.OK.User.ID) +} + +func TestListUsersByAccountInfoWithDoUnion(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + tpuser, err := thirdparty.ManuallyCreateOrUpdateUser("public", "google", "abc", "test1@gmail.com") + if err != nil { + t.Error(err) + return + } + + epuser, err := SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + email := "test@gmail.com" + users, err := supertokens.ListUsersByAccountInfo("public", supertokens.AccountInfo{ + Email: &email, + ThirdParty: &supertokens.ThirdParty{ + ID: "google", + UserID: "abc", + }, + }, false) + if err != nil { + t.Error(err) + return + } + assert.Len(t, users, 0) + + users, err = supertokens.ListUsersByAccountInfo("public", supertokens.AccountInfo{ + Email: &email, + ThirdParty: &supertokens.ThirdParty{ + ID: "google", + UserID: "abc", + }, + }, true) + if err != nil { + t.Error(err) + return + } + assert.Len(t, users, 2) + + assert.Equal(t, users[0].ID, tpuser.OK.User.ID) + assert.Equal(t, users[1].ID, epuser.OK.User.ID) +} + +func TestListUsersByAccountInfoTakesIntoAccountTenantId(t *testing.T) { + BeforeEach() + unittesting.StartUpST("localhost", "8080") + defer AfterEach() + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + session.Init(nil), + Init(nil), + emailverification.Init(evmodels.TypeInput{ + Mode: evmodels.ModeRequired, + }), + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) + + enabled := true + + multitenancy.CreateOrUpdateTenant("t1", multitenancymodels.TenantConfig{ + ThirdPartyEnabled: &enabled, + }) + + tpuser, err := thirdparty.ManuallyCreateOrUpdateUser("t1", "google", "abc", "test@gmail.com") + if err != nil { + t.Error(err) + return + } + + _, err = SignUp("public", "test@gmail.com", "pass123") + if err != nil { + t.Error(err) + return + } + + email := "test@gmail.com" + users, err := supertokens.ListUsersByAccountInfo("t1", supertokens.AccountInfo{ + Email: &email, + }, false) + if err != nil { + t.Error(err) + return + } + assert.Len(t, users, 1) + + assert.Equal(t, users[0].ID, tpuser.OK.User.ID) +} + +func TestThatEmailProxyNotSetIfEmailVerificationNotInit(t *testing.T) { + telemetry := false + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "http://localhost:8080", + }, + AppInfo: supertokens.AppInfo{ + AppName: "Testing", + Origin: "http://localhost:3000", + APIDomain: "http://localhost:3001", + }, + Telemetry: &telemetry, + RecipeList: []supertokens.Recipe{ + Init(nil), + }, + }) + + assert.Nil(t, supertokens.InternalUseEmailVerificationRecipeProxyInstance) +} + +// TODO: remove this function +func convertEpUserToSuperTokensUser(epuser epmodels.User) supertokens.User { + rUId, err := supertokens.NewRecipeUserID(epuser.ID) + if err != nil { + panic(err.Error()) + } + return supertokens.User{ + ID: epuser.ID, + TimeJoined: epuser.TimeJoined, + IsPrimaryUser: false, + TenantIDs: epuser.TenantIds, + Emails: []string{epuser.Email}, + PhoneNumbers: []string{}, + ThirdParty: []supertokens.ThirdParty{}, + LoginMethods: []supertokens.LoginMethods{ + { + Verified: false, + RecipeLevelUser: supertokens.RecipeLevelUser{ + TenantIDs: epuser.TenantIds, + TimeJoined: epuser.TimeJoined, + RecipeUserID: rUId, + AccountInfoWithRecipeID: supertokens.AccountInfoWithRecipeID{ + RecipeID: supertokens.EmailPasswordRID, + AccountInfo: supertokens.AccountInfo{ + Email: &epuser.Email, + }, + }, + }, + }, + }, + } +} + +// TODO: remove this function +func convertTpUserToSuperTokensUser(tpuser tpmodels.User) supertokens.User { + rUId, err := supertokens.NewRecipeUserID(tpuser.ID) + if err != nil { + panic(err.Error()) + } + return supertokens.User{ + ID: tpuser.ID, + TimeJoined: tpuser.TimeJoined, + IsPrimaryUser: false, + TenantIDs: tpuser.TenantIds, + Emails: []string{tpuser.Email}, + PhoneNumbers: []string{}, + ThirdParty: []supertokens.ThirdParty{ + { + ID: tpuser.ThirdParty.ID, + UserID: tpuser.ThirdParty.UserID, + }, + }, + LoginMethods: []supertokens.LoginMethods{ + { + Verified: false, + RecipeLevelUser: supertokens.RecipeLevelUser{ + TenantIDs: tpuser.TenantIds, + TimeJoined: tpuser.TimeJoined, + RecipeUserID: rUId, + AccountInfoWithRecipeID: supertokens.AccountInfoWithRecipeID{ + RecipeID: supertokens.EmailPasswordRID, + AccountInfo: supertokens.AccountInfo{ + Email: &tpuser.Email, + ThirdParty: &supertokens.ThirdParty{ + ID: tpuser.ThirdParty.ID, + UserID: tpuser.ThirdParty.UserID, + }, + }, + }, + }, + }, + }, + } +} diff --git a/recipe/emailpassword/deleteUser_test.go b/recipe/emailpassword/deleteUser_test.go deleted file mode 100644 index 5e1c1891..00000000 --- a/recipe/emailpassword/deleteUser_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package emailpassword - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" - "github.com/supertokens/supertokens-golang/supertokens" - "github.com/supertokens/supertokens-golang/test/unittesting" -) - -func TestDeleteUser(t *testing.T) { - configValue := supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "http://localhost:8080", - }, - AppInfo: supertokens.AppInfo{ - APIDomain: "api.supertokens.io", - AppName: "SuperTokens", - WebsiteDomain: "supertokens.io", - }, - RecipeList: []supertokens.Recipe{ - Init(nil), - session.Init(&sessmodels.TypeInput{ - GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { - return sessmodels.CookieTransferMethod - }, - }), - }, - } - BeforeEach() - unittesting.StartUpST("localhost", "8080") - defer AfterEach() - err := supertokens.Init(configValue) - if err != nil { - t.Error(err.Error()) - } - querier, err := supertokens.GetNewQuerierInstanceOrThrowError("") - if err != nil { - t.Error(err.Error()) - } - cdiVersion, err := querier.GetQuerierAPIVersion() - if err != nil { - t.Error(err.Error()) - } - if unittesting.MaxVersion("2.10", cdiVersion) == cdiVersion { - res, err := SignUp("public", "test@example.com", "1234abcd") - if err != nil { - t.Error(err.Error()) - } - reponseBeforeDeletingUser, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 1, len(reponseBeforeDeletingUser.Users)) - err = supertokens.DeleteUser(res.OK.User.ID) - if err != nil { - t.Error(err.Error()) - } - reponseAfterDeletingUser, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 0, len(reponseAfterDeletingUser.Users)) - } -} diff --git a/recipe/emailpassword/emailExistsAndVerificationCheck_test.go b/recipe/emailpassword/emailExistsAndVerificationCheck_test.go index aa206463..b3f19205 100644 --- a/recipe/emailpassword/emailExistsAndVerificationCheck_test.go +++ b/recipe/emailpassword/emailExistsAndVerificationCheck_test.go @@ -1854,7 +1854,7 @@ func TestEmailVerifyWithDeletedUser(t *testing.T) { userId := response["user"].(map[string]interface{})["id"] cookieData := unittesting.ExtractInfoFromResponse(resp) - supertokens.DeleteUser(userId.(string)) + supertokens.DeleteUser(userId.(string), true) resp1, err := unittesting.EmailVerifyTokenRequest(testServer.URL, userId.(string), cookieData["sAccessToken"], cookieData["antiCsrf"]) diff --git a/recipe/emailpassword/userIdMapping_users_test.go b/recipe/emailpassword/userIdMapping_users_test.go index 6c70074f..166b14c3 100644 --- a/recipe/emailpassword/userIdMapping_users_test.go +++ b/recipe/emailpassword/userIdMapping_users_test.go @@ -37,7 +37,7 @@ func TestCreateUserIdMappingAndDeleteUser(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, createResp.OK) - err = supertokens.DeleteUser(externalUserId) + err = supertokens.DeleteUser(externalUserId, true) assert.NoError(t, err) } @@ -76,6 +76,6 @@ func TestCreateUserIdMappingAndGetUsers(t *testing.T) { assert.NotNil(t, userResult.Users) for i, user := range userResult.Users { - assert.Equal(t, user.User["id"], fmt.Sprintf("externalId%d", i)) + assert.Equal(t, user.ID, fmt.Sprintf("externalId%d", i)) } } diff --git a/recipe/emailpassword/user_test.go b/recipe/emailpassword/user_test.go index e7ed5563..b7ef3755 100644 --- a/recipe/emailpassword/user_test.go +++ b/recipe/emailpassword/user_test.go @@ -28,219 +28,6 @@ import ( "github.com/supertokens/supertokens-golang/test/unittesting" ) -func TestGetUsersOldestFirst(t *testing.T) { - configValue := supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "http://localhost:8080", - }, - AppInfo: supertokens.AppInfo{ - APIDomain: "api.supertokens.io", - AppName: "SuperTokens", - WebsiteDomain: "supertokens.io", - }, - RecipeList: []supertokens.Recipe{ - Init(nil), - session.Init(&sessmodels.TypeInput{ - GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { - return sessmodels.CookieTransferMethod - }, - }), - }, - } - - BeforeEach() - unittesting.StartUpST("localhost", "8080") - defer AfterEach() - err := supertokens.Init(configValue) - if err != nil { - - t.Error(err.Error()) - } - mux := http.NewServeMux() - testServer := httptest.NewServer(supertokens.Middleware(mux)) - defer testServer.Close() - - _, err = unittesting.SignupRequest("test@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test1@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test2@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test3@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test4@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - - users, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 5) - assert.Nil(t, users.NextPaginationToken) - - limit := 1 - users, err = supertokens.GetUsersOldestFirst("public", nil, &limit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 1) - assert.NotNil(t, users.NextPaginationToken) - assert.Equal(t, "test@gmail.com", users.Users[0].User["email"]) - - users, err = supertokens.GetUsersOldestFirst("public", users.NextPaginationToken, &limit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 1) - assert.NotNil(t, users.NextPaginationToken) - assert.Equal(t, "test1@gmail.com", users.Users[0].User["email"]) - - limit = 5 - users, err = supertokens.GetUsersOldestFirst("public", users.NextPaginationToken, &limit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 3) - assert.Nil(t, users.NextPaginationToken) - - customPaginationToken := "invalid-pagination-token" - limit = 10 - users, err = supertokens.GetUsersOldestFirst("public", &customPaginationToken, &limit, nil, nil) - if err != nil { - assert.Equal(t, "SuperTokens core threw an error for a request to path: '/public/users' with status code: 400 and message: invalid pagination token\n", err.Error()) - } else { - t.Fail() - } - - limit = -1 - users, err = supertokens.GetUsersOldestFirst("public", nil, &limit, nil, nil) - if err != nil { - assert.Equal(t, "SuperTokens core threw an error for a request to path: '/public/users' with status code: 400 and message: limit must a positive integer with min value 1\n", err.Error()) - } else { - t.Fail() - } -} - -func TestGetUsersNewestFirst(t *testing.T) { - configValue := supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "http://localhost:8080", - }, - AppInfo: supertokens.AppInfo{ - APIDomain: "api.supertokens.io", - AppName: "SuperTokens", - WebsiteDomain: "supertokens.io", - }, - RecipeList: []supertokens.Recipe{ - Init(nil), - session.Init(&sessmodels.TypeInput{ - GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { - return sessmodels.CookieTransferMethod - }, - }), - }, - } - - BeforeEach() - unittesting.StartUpST("localhost", "8080") - defer AfterEach() - err := supertokens.Init(configValue) - if err != nil { - t.Error(err.Error()) - } - mux := http.NewServeMux() - testServer := httptest.NewServer(supertokens.Middleware(mux)) - defer testServer.Close() - - _, err = unittesting.SignupRequest("test@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test1@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test2@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test3@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - _, err = unittesting.SignupRequest("test4@gmail.com", "testPass123", testServer.URL) - if err != nil { - t.Error(err.Error()) - } - - users, err := supertokens.GetUsersNewestFirst("public", nil, nil, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 5) - assert.Nil(t, users.NextPaginationToken) - - limit := 1 - users, err = supertokens.GetUsersNewestFirst("public", nil, &limit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 1) - assert.NotNil(t, users.NextPaginationToken) - assert.Equal(t, "test4@gmail.com", users.Users[0].User["email"]) - - users, err = supertokens.GetUsersNewestFirst("public", users.NextPaginationToken, &limit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 1) - assert.NotNil(t, users.NextPaginationToken) - assert.Equal(t, "test3@gmail.com", users.Users[0].User["email"]) - - limit = 5 - users, err = supertokens.GetUsersNewestFirst("public", users.NextPaginationToken, &limit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, len(users.Users), 3) - assert.Nil(t, users.NextPaginationToken) - - customPaginationToken := "invalid-pagination-token" - limit = 10 - users, err = supertokens.GetUsersNewestFirst("public", &customPaginationToken, &limit, nil, nil) - if err != nil { - assert.Equal(t, "SuperTokens core threw an error for a request to path: '/public/users' with status code: 400 and message: invalid pagination token\n", err.Error()) - } else { - t.Fail() - } - - limit = -1 - users, err = supertokens.GetUsersNewestFirst("public", nil, &limit, nil, nil) - if err != nil { - assert.Equal(t, "SuperTokens core threw an error for a request to path: '/public/users' with status code: 400 and message: limit must a positive integer with min value 1\n", err.Error()) - } else { - t.Fail() - } -} - func TestGetUserCount(t *testing.T) { configValue := supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ diff --git a/recipe/emailpassword/utils.go b/recipe/emailpassword/utils.go index d6ea3d7f..add84779 100644 --- a/recipe/emailpassword/utils.go +++ b/recipe/emailpassword/utils.go @@ -260,10 +260,19 @@ func parseUser(value interface{}) (*epmodels.User, error) { if err != nil { return nil, err } - var user epmodels.User - err = json.Unmarshal(respJSON, &user) + var supertokensUser supertokens.User + err = json.Unmarshal(respJSON, &supertokensUser) + if err != nil { return nil, err } - return &user, nil + + epUser := epmodels.User{ + ID: supertokensUser.ID, + Email: supertokensUser.Emails[0], + TimeJoined: supertokensUser.TimeJoined, + TenantIds: supertokensUser.TenantIDs, + } + + return &epUser, nil } diff --git a/recipe/emailverification/recipe.go b/recipe/emailverification/recipe.go index f1c02bb6..ec43011f 100644 --- a/recipe/emailverification/recipe.go +++ b/recipe/emailverification/recipe.go @@ -101,6 +101,8 @@ func MakeRecipe(recipeId string, appInfo supertokens.NormalisedAppinfo, config e getEmailForUserIdFuncsFromOtherRecipes = append(getEmailForUserIdFuncsFromOtherRecipes, function) } + assignToSuperTokensPackage(r) + return *r, nil } @@ -212,3 +214,59 @@ func ResetForTest() { UserContext supertokens.UserContext }{} } + +func assignToSuperTokensPackage(r *Recipe) { + // This is there so that the supertokens package can use the email verification + // recipe since account linking requires it. We cannot directly use the recipe from there + // cause of cyclic dependencies. + supertokens.InternalUseEmailVerificationRecipeProxyInstance = &supertokens.InternalUseEmailVerificationRecipeProxy{ + CreateEmailVerificationToken: func(userID supertokens.RecipeUserID, email, tenantId string, userContext supertokens.UserContext) (supertokens.InternalUseCreateEmailVerificationTokenResponse, error) { + resp, err := (*r.RecipeImpl.CreateEmailVerificationToken)(userID.GetAsString(), email, tenantId, userContext) + if err != nil { + return supertokens.InternalUseCreateEmailVerificationTokenResponse{}, err + } + return supertokens.InternalUseCreateEmailVerificationTokenResponse{ + OK: resp.OK, + EmailAlreadyVerifiedError: resp.EmailAlreadyVerifiedError, + }, nil + }, + VerifyEmailUsingToken: func(token, tenantId string, attemptAccountLinking bool, userContext supertokens.UserContext) (supertokens.InternalUseVerifyEmailUsingTokenResponse, error) { + resp, err := (*r.RecipeImpl.VerifyEmailUsingToken)(token, tenantId, userContext) + if err != nil { + return supertokens.InternalUseVerifyEmailUsingTokenResponse{}, err + } + return supertokens.InternalUseVerifyEmailUsingTokenResponse{ + OK: &struct { + User supertokens.InternalUseEmailVerificationUser + }{ + User: supertokens.InternalUseEmailVerificationUser{ + ID: resp.OK.User.ID, + Email: resp.OK.User.Email, + }, + }, + EmailVerificationInvalidTokenError: resp.EmailVerificationInvalidTokenError, + }, nil + }, + IsEmailVerified: func(userID, email string, userContext supertokens.UserContext) (bool, error) { + return (*r.RecipeImpl.IsEmailVerified)(userID, email, userContext) + }, + RevokeEmailVerificationTokens: func(userId, email, tenantId string, userContext supertokens.UserContext) (supertokens.InternalUseRevokeEmailVerificationTokensResponse, error) { + resp, err := (*r.RecipeImpl.RevokeEmailVerificationTokens)(userId, email, tenantId, userContext) + if err != nil { + return supertokens.InternalUseRevokeEmailVerificationTokensResponse{}, err + } + return supertokens.InternalUseRevokeEmailVerificationTokensResponse{ + OK: resp.OK, + }, nil + }, + UnverifyEmail: func(userId, email string, userContext supertokens.UserContext) (supertokens.InternalUseUnverifyEmailResponse, error) { + resp, err := (*r.RecipeImpl.UnverifyEmail)(userId, email, userContext) + if err != nil { + return supertokens.InternalUseUnverifyEmailResponse{}, err + } + return supertokens.InternalUseUnverifyEmailResponse{ + OK: resp.OK, + }, nil + }, + } +} diff --git a/recipe/multitenancy/recipe.go b/recipe/multitenancy/recipe.go index 7c514829..96f45e04 100644 --- a/recipe/multitenancy/recipe.go +++ b/recipe/multitenancy/recipe.go @@ -164,7 +164,7 @@ func (r *Recipe) SetStaticThirdPartyProviders(providers []tpmodels.ProviderInput // the supertokens Init can create an instance of the multitenancy recipe automatically // if the user has not explicitly created one. func init() { - supertokens.DefaultMultitenancyRecipe = recipeInit(nil) + supertokens.InternalUseDefaultMultitenancyRecipe = recipeInit(nil) // Create multitenancy claims when the module is imported multitenancyclaims.AllowedDomainsClaim, multitenancyclaims.AllowedDomainsClaimValidators = NewAllowedDomainsClaim() diff --git a/recipe/thirdparty/recipeImplementation.go b/recipe/thirdparty/recipeImplementation.go index b499b7ca..5b7103af 100644 --- a/recipe/thirdparty/recipeImplementation.go +++ b/recipe/thirdparty/recipeImplementation.go @@ -50,7 +50,10 @@ func MakeRecipeImplementation(querier supertokens.Querier, providers []tpmodels. response, err := querier.SendPostRequest(tenantId+"/recipe/signinup", map[string]interface{}{ "thirdPartyId": thirdPartyID, "thirdPartyUserId": thirdPartyUserID, - "email": map[string]interface{}{"id": email}, + "email": map[string]interface{}{ + "id": email, + "isVerified": false, // TODO: properly implement this + }, }, userContext) if err != nil { return tpmodels.SignInUpResponse{}, err @@ -78,7 +81,10 @@ func MakeRecipeImplementation(querier supertokens.Querier, providers []tpmodels. response, err := querier.SendPostRequest(tenantId+"/recipe/signinup", map[string]interface{}{ "thirdPartyId": thirdPartyID, "thirdPartyUserId": thirdPartyUserID, - "email": map[string]interface{}{"id": email}, + "email": map[string]interface{}{ + "id": email, + "isVerified": false, // TODO: properly implement this + }, }, userContext) if err != nil { return tpmodels.ManuallyCreateOrUpdateUserResponse{}, err diff --git a/recipe/thirdparty/users_test.go b/recipe/thirdparty/users_test.go deleted file mode 100644 index cc55c191..00000000 --- a/recipe/thirdparty/users_test.go +++ /dev/null @@ -1,353 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package thirdparty - -import ( - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" - "github.com/supertokens/supertokens-golang/test/unittesting" -) - -func TestGetUsersOldesFirst(t *testing.T) { - configValue := supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "http://localhost:8080", - }, - AppInfo: supertokens.AppInfo{ - APIDomain: "api.supertokens.io", - AppName: "SuperTokens", - WebsiteDomain: "supertokens.io", - }, - RecipeList: []supertokens.Recipe{ - session.Init(&sessmodels.TypeInput{ - GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { - return sessmodels.CookieTransferMethod - }, - }), - Init( - &tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - customProvider2, - }, - }, - }, - ), - }, - } - - BeforeEach() - unittesting.StartUpST("localhost", "8080") - defer AfterEach() - err := supertokens.Init(configValue) - - if err != nil { - t.Error(err.Error()) - } - - mux := http.NewServeMux() - testServer := httptest.NewServer(supertokens.Middleware(mux)) - defer testServer.Close() - - unittesting.SigninupCustomRequest(testServer.URL, "test@gmail.com", "testPass0") - unittesting.SigninupCustomRequest(testServer.URL, "test1@gmail.com", "testPass1") - unittesting.SigninupCustomRequest(testServer.URL, "john@gmail.com", "testPass2") - unittesting.SigninupCustomRequest(testServer.URL, "test3@gmail.com", "testPass3") - unittesting.SigninupCustomRequest(testServer.URL, "test4@gmail.com", "testPass4") - - userPaginationResult, err := supertokens.GetUsersOldestFirst("public", nil, nil, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 5, len(userPaginationResult.Users)) - assert.Nil(t, userPaginationResult.NextPaginationToken) - - customLimit := 1 - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", nil, &customLimit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 1, len(userPaginationResult.Users)) - assert.Equal(t, "test@gmail.com", userPaginationResult.Users[0].User["email"]) - assert.Equal(t, "*string", reflect.TypeOf(userPaginationResult.NextPaginationToken).String()) - - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", userPaginationResult.NextPaginationToken, &customLimit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 1, len(userPaginationResult.Users)) - assert.Equal(t, "test1@gmail.com", userPaginationResult.Users[0].User["email"]) - assert.Equal(t, "*string", reflect.TypeOf(userPaginationResult.NextPaginationToken).String()) - - customLimit = 5 - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", userPaginationResult.NextPaginationToken, &customLimit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 3, len(userPaginationResult.Users)) - - customInvalidPaginationToken := "invalid-pagination-token" - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", &customInvalidPaginationToken, &customLimit, nil, nil) - if err != nil { - assert.Contains(t, err.Error(), "invalid pagination token") - } else { - t.Fail() - } - - customLimit = -1 - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", nil, &customLimit, nil, nil) - if err != nil { - assert.Contains(t, err.Error(), "limit must a positive integer with min value 1") - } else { - t.Fail() - } - - querier, err := supertokens.GetNewQuerierInstanceOrThrowError("thirdparty") - if err != nil { - t.Fail() - } - cdiVersion, err := querier.GetQuerierAPIVersion() - if err != nil { - t.Fail() - } - - if supertokens.MaxVersion(cdiVersion, "2.20") != cdiVersion { - t.Skip() - } - - customLimit = 10 - query := make(map[string]string) - query["email"] = "doe" - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", nil, &customLimit, nil, query) - if err != nil { - t.Fail() - } else { - assert.Equal(t, len(userPaginationResult.Users), 0) - } - - query["email"] = "john" - userPaginationResult, err = supertokens.GetUsersOldestFirst("public", nil, &customLimit, nil, query) - if err != nil { - t.Fail() - } else { - assert.Equal(t, len(userPaginationResult.Users), 1) - } -} - -func TestGetUsersNewestFirst(t *testing.T) { - configValue := supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "http://localhost:8080", - }, - AppInfo: supertokens.AppInfo{ - APIDomain: "api.supertokens.io", - AppName: "SuperTokens", - WebsiteDomain: "supertokens.io", - }, - RecipeList: []supertokens.Recipe{ - session.Init(&sessmodels.TypeInput{ - GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { - return sessmodels.CookieTransferMethod - }, - }), - Init( - &tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - customProvider2, - }, - }, - }, - ), - }, - } - - BeforeEach() - unittesting.StartUpST("localhost", "8080") - defer AfterEach() - err := supertokens.Init(configValue) - - if err != nil { - t.Error(err.Error()) - } - - mux := http.NewServeMux() - testServer := httptest.NewServer(supertokens.Middleware(mux)) - defer testServer.Close() - - unittesting.SigninupCustomRequest(testServer.URL, "test@gmail.com", "testPass0") - unittesting.SigninupCustomRequest(testServer.URL, "test1@gmail.com", "testPass1") - unittesting.SigninupCustomRequest(testServer.URL, "john@gmail.com", "testPass2") - unittesting.SigninupCustomRequest(testServer.URL, "test3@gmail.com", "testPass3") - unittesting.SigninupCustomRequest(testServer.URL, "test4@gmail.com", "testPass4") - - userPaginationResult, err := supertokens.GetUsersNewestFirst("public", nil, nil, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 5, len(userPaginationResult.Users)) - assert.Nil(t, userPaginationResult.NextPaginationToken) - - customLimit := 1 - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", nil, &customLimit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 1, len(userPaginationResult.Users)) - assert.Equal(t, "test4@gmail.com", userPaginationResult.Users[0].User["email"]) - assert.Equal(t, "*string", reflect.TypeOf(userPaginationResult.NextPaginationToken).String()) - - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", userPaginationResult.NextPaginationToken, &customLimit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 1, len(userPaginationResult.Users)) - assert.Equal(t, "test3@gmail.com", userPaginationResult.Users[0].User["email"]) - assert.Equal(t, "*string", reflect.TypeOf(userPaginationResult.NextPaginationToken).String()) - - customLimit = 5 - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", userPaginationResult.NextPaginationToken, &customLimit, nil, nil) - if err != nil { - t.Error(err.Error()) - } - assert.Equal(t, 3, len(userPaginationResult.Users)) - - customInvalidPaginationToken := "invalid-pagination-token" - customLimit = 10 - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", &customInvalidPaginationToken, &customLimit, nil, nil) - if err != nil { - assert.Contains(t, err.Error(), "invalid pagination token") - } else { - t.Fail() - } - - customLimit = -1 - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", nil, &customLimit, nil, nil) - if err != nil { - assert.Contains(t, err.Error(), "limit must a positive integer with min value 1") - } else { - t.Fail() - } - - querier, err := supertokens.GetNewQuerierInstanceOrThrowError("thirdparty") - if err != nil { - t.Fail() - } - cdiVersion, err := querier.GetQuerierAPIVersion() - if err != nil { - t.Fail() - } - - if supertokens.MaxVersion(cdiVersion, "2.20") != cdiVersion { - t.Skip() - } - - customLimit = 10 - query := make(map[string]string) - query["email"] = "doe" - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", nil, &customLimit, nil, query) - if err != nil { - t.Fail() - } else { - assert.Equal(t, len(userPaginationResult.Users), 0) - } - - query["email"] = "john" - userPaginationResult, err = supertokens.GetUsersNewestFirst("public", nil, &customLimit, nil, query) - if err != nil { - t.Fail() - } else { - assert.Equal(t, len(userPaginationResult.Users), 1) - } -} - -func TestGetUserCount(t *testing.T) { - configValue := supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "http://localhost:8080", - }, - AppInfo: supertokens.AppInfo{ - APIDomain: "api.supertokens.io", - AppName: "SuperTokens", - WebsiteDomain: "supertokens.io", - }, - RecipeList: []supertokens.Recipe{ - session.Init(&sessmodels.TypeInput{ - GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { - return sessmodels.CookieTransferMethod - }, - }), - Init( - &tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - customProvider2, - }, - }, - }, - ), - }, - } - - BeforeEach() - unittesting.StartUpST("localhost", "8080") - defer AfterEach() - err := supertokens.Init(configValue) - - if err != nil { - t.Error(err.Error()) - } - - mux := http.NewServeMux() - testServer := httptest.NewServer(supertokens.Middleware(mux)) - defer testServer.Close() - - userCount, err := supertokens.GetUserCount(nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, 0.0, userCount) - - unittesting.SigninupCustomRequest(testServer.URL, "test@gmail.com", "testPass0") - - userCount, err = supertokens.GetUserCount(nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, 1.0, userCount) - - unittesting.SigninupCustomRequest(testServer.URL, "test1@gmail.com", "testPass1") - unittesting.SigninupCustomRequest(testServer.URL, "test2@gmail.com", "testPass2") - unittesting.SigninupCustomRequest(testServer.URL, "test3@gmail.com", "testPass3") - unittesting.SigninupCustomRequest(testServer.URL, "test4@gmail.com", "testPass4") - - userCount, err = supertokens.GetUserCount(nil, nil) - if err != nil { - t.Error(err.Error()) - } - - assert.Equal(t, 5.0, userCount) -} diff --git a/recipe/thirdparty/utils.go b/recipe/thirdparty/utils.go index dbef81d1..1320e21d 100644 --- a/recipe/thirdparty/utils.go +++ b/recipe/thirdparty/utils.go @@ -84,12 +84,28 @@ func parseUser(value interface{}) (*tpmodels.User, error) { if err != nil { return nil, err } - var user tpmodels.User - err = json.Unmarshal(respJSON, &user) + var supertokensUser supertokens.User + err = json.Unmarshal(respJSON, &supertokensUser) + if err != nil { return nil, err } - return &user, nil + + tpUser := tpmodels.User{ + ID: supertokensUser.ID, + Email: supertokensUser.Emails[0], + TimeJoined: supertokensUser.TimeJoined, + TenantIds: supertokensUser.TenantIDs, + ThirdParty: struct { + ID string `json:"id"` + UserID string `json:"userId"` + }{ + ID: supertokensUser.ThirdParty[0].ID, + UserID: supertokensUser.ThirdParty[0].UserID, + }, + } + + return &tpUser, nil } func parseUsers(value interface{}) ([]tpmodels.User, error) { diff --git a/supertokens/accountlinkingRecipe.go b/supertokens/accountlinkingRecipe.go new file mode 100644 index 00000000..0d1ca5a1 --- /dev/null +++ b/supertokens/accountlinkingRecipe.go @@ -0,0 +1,150 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package supertokens + +import ( + "errors" + "net/http" +) + +const RECIPE_ID = "accountlinking" + +type AccountLinkingRecipe struct { + RecipeModule RecipeModule + Config AccountLinkingTypeNormalisedInput + RecipeImpl AccountLinkingRecipeInterface +} + +var singletonInstance *AccountLinkingRecipe + +func makeAccountLinkingRecipe(recipeId string, appInfo NormalisedAppinfo, config *AccountLinkingTypeInput, onSuperTokensAPIError func(err error, req *http.Request, res http.ResponseWriter)) (AccountLinkingRecipe, error) { + r := &AccountLinkingRecipe{} + verifiedConfig := validateAndNormaliseAccountLinkingUserInput(appInfo, config) + r.Config = verifiedConfig + + querierInstance, err := GetNewQuerierInstanceOrThrowError(recipeId) + if err != nil { + return AccountLinkingRecipe{}, err + } + recipeImplementation := makeRecipeImplementation(*querierInstance, verifiedConfig) + r.RecipeImpl = verifiedConfig.Override.Functions(recipeImplementation) + + recipeModuleInstance := MakeRecipeModule(recipeId, appInfo, r.handleAPIRequest, r.getAllCORSHeaders, r.getAPIsHandled, nil, r.handleError, onSuperTokensAPIError) + r.RecipeModule = recipeModuleInstance + + return *r, nil +} + +func getAccountLinkingRecipeInstanceOrThrowError() (*AccountLinkingRecipe, error) { + if singletonInstance != nil { + return singletonInstance, nil + } + return nil, errors.New("Initialisation not done. Did you forget to call the init function?") +} + +func accountLinkingRecipeInit(config *AccountLinkingTypeInput) Recipe { + return func(appInfo NormalisedAppinfo, onSuperTokensAPIError func(err error, req *http.Request, res http.ResponseWriter)) (*RecipeModule, error) { + if singletonInstance == nil { + recipe, err := makeAccountLinkingRecipe(RECIPE_ID, appInfo, config, onSuperTokensAPIError) + if err != nil { + return nil, err + } + singletonInstance = &recipe + + return &singletonInstance.RecipeModule, nil + } + return nil, errors.New("Account linking recipe has already been initialised. Please check your code for bugs.") + } +} + +// implement RecipeModule + +func (r *AccountLinkingRecipe) getAPIsHandled() ([]APIHandled, error) { + return []APIHandled{}, nil +} + +func (r *AccountLinkingRecipe) handleAPIRequest(id string, tenantId string, req *http.Request, res http.ResponseWriter, theirHandler http.HandlerFunc, _ NormalisedURLPath, _ string, userContext UserContext) error { + return errors.New("should never come here") +} + +func (r *AccountLinkingRecipe) getAllCORSHeaders() []string { + return []string{} +} + +func (r *AccountLinkingRecipe) handleError(err error, req *http.Request, res http.ResponseWriter, userContext UserContext) (bool, error) { + return false, nil +} + +func verifyEmailForRecipeUserIfLinkedAccountsAreVerified(user User, recipeUserId RecipeUserID, userContext UserContext) error { + if InternalUseEmailVerificationRecipeProxyInstance == nil { + // if email verification recipe is not initialised, then no op + return nil + } + + // This is just a helper function cause it's called in many places + // like during sign up, sign in and post linking accounts. + // This is not exposed to the developer as it's called in the relevant + // recipe functions. + // We do not do this in the core cause email verification is a different + // recipe. + // Finally, we only mark the email of this recipe user as verified and not + // the other recipe users in the primary user (if this user's email is verified), + // cause when those other users sign in, this function will be called for them anyway. + + if user.IsPrimaryUser { + var recipeUserEmail *string = nil + isAlreadyVerified := false + for _, method := range user.LoginMethods { + if method.RecipeUserID.GetAsString() == recipeUserId.GetAsString() { + recipeUserEmail = method.Email + isAlreadyVerified = method.Verified + } + } + + if recipeUserEmail != nil { + if isAlreadyVerified { + return nil + } + + shouldVerifyEmail := false + for _, method := range user.LoginMethods { + if method.HasSameEmailAs(recipeUserEmail) && method.Verified { + shouldVerifyEmail = true + } + } + + if shouldVerifyEmail { + // While the token we create here is tenant specific, the verification status is not + // So we can use any tenantId the user is associated with here as long as we use the + // same in the verifyEmailUsingToken call + token, err := InternalUseEmailVerificationRecipeProxyInstance.CreateEmailVerificationToken(recipeUserId, *recipeUserEmail, user.TenantIDs[0], userContext) + if err != nil { + return err + } + if token.OK != nil { + // we purposely pass in false below cause we don't want account + // linking to happen + _, err := InternalUseEmailVerificationRecipeProxyInstance.VerifyEmailUsingToken(token.OK.Token, user.TenantIDs[0], false, userContext) + if err != nil { + return err + } + } + } + } + } + + return nil +} diff --git a/supertokens/accountlinkingRecipeImplementation.go b/supertokens/accountlinkingRecipeImplementation.go new file mode 100644 index 00000000..131b0e65 --- /dev/null +++ b/supertokens/accountlinkingRecipeImplementation.go @@ -0,0 +1,397 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package supertokens + +import ( + "encoding/json" + "errors" + "strconv" + "strings" +) + +func makeRecipeImplementation(querier Querier, config AccountLinkingTypeNormalisedInput) AccountLinkingRecipeInterface { + + getUsers := func(tenantID string, timeJoinedOrder string, paginationToken *string, limit *int, includeRecipeIds *[]string, searchParams map[string]string, userContext UserContext) (UserPaginationResult, error) { + requestBody := map[string]string{} + if searchParams != nil { + requestBody = searchParams + } + requestBody["timeJoinedOrder"] = timeJoinedOrder + + if limit != nil { + requestBody["limit"] = strconv.Itoa(*limit) + } + if paginationToken != nil { + requestBody["paginationToken"] = *paginationToken + } + if includeRecipeIds != nil { + requestBody["includeRecipeIds"] = strings.Join((*includeRecipeIds)[:], ",") + } + + resp, err := querier.SendGetRequest(tenantID+"/users", requestBody, userContext) + + if err != nil { + return UserPaginationResult{}, err + } + + temporaryVariable, err := json.Marshal(resp) + if err != nil { + return UserPaginationResult{}, err + } + + var result = UserPaginationResult{} + + err = json.Unmarshal(temporaryVariable, &result) + + if err != nil { + return UserPaginationResult{}, err + } + + return result, nil + } + + getUser := func(userId string, userContext UserContext) (*User, error) { + requestBody := map[string]string{ + "userId": userId, + } + resp, err := querier.SendGetRequest("/user/id", requestBody, userContext) + + if err != nil { + return nil, err + } + + if resp["status"].(string) != "OK" { + return nil, nil + } + + var result = User{} + + temporaryVariable, err := json.Marshal(resp["user"]) + if err != nil { + return nil, err + } + + err = json.Unmarshal(temporaryVariable, &result) + if err != nil { + return nil, err + } + + return &result, nil + } + + canCreatePrimaryUser := func(recipeUserId RecipeUserID, userContext UserContext) (CanCreatePrimaryUserResponse, error) { + requestBody := map[string]string{ + "recipeUserId": recipeUserId.GetAsString(), + } + resp, err := querier.SendGetRequest("/recipe/accountlinking/user/primary/check", requestBody, userContext) + + if err != nil { + return CanCreatePrimaryUserResponse{}, err + } + + if resp["status"].(string) == "OK" { + return CanCreatePrimaryUserResponse{ + OK: &struct{ WasAlreadyAPrimaryUser bool }{ + WasAlreadyAPrimaryUser: resp["wasAlreadyAPrimaryUser"].(bool), + }, + }, nil + } else if resp["status"].(string) == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" { + return CanCreatePrimaryUserResponse{ + RecipeUserIdAlreadyLinkedWithPrimaryUserIdError: &struct { + PrimaryUserId string + Description string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + Description: resp["description"].(string), + }, + }, nil + } else { + return CanCreatePrimaryUserResponse{ + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError: &struct { + PrimaryUserId string + Description string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + Description: resp["description"].(string), + }, + }, nil + } + } + + createPrimaryUser := func(recipeUserId RecipeUserID, userContext UserContext) (CreatePrimaryUserResponse, error) { + requestBody := map[string]interface{}{ + "recipeUserId": recipeUserId.GetAsString(), + } + resp, err := querier.SendPostRequest("/recipe/accountlinking/user/primary", requestBody, userContext) + + if err != nil { + return CreatePrimaryUserResponse{}, err + } + + if resp["status"].(string) == "OK" { + var user = User{} + + temporaryVariable, err := json.Marshal(resp["user"]) + if err != nil { + return CreatePrimaryUserResponse{}, err + } + + err = json.Unmarshal(temporaryVariable, &user) + if err != nil { + return CreatePrimaryUserResponse{}, err + } + return CreatePrimaryUserResponse{ + OK: &struct { + User User + WasAlreadyAPrimaryUser bool + }{ + WasAlreadyAPrimaryUser: resp["wasAlreadyAPrimaryUser"].(bool), + User: user, + }, + }, nil + } else if resp["status"].(string) == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" { + return CreatePrimaryUserResponse{ + RecipeUserIdAlreadyLinkedWithPrimaryUserIdError: &struct { + PrimaryUserId string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + }, + }, nil + } else { + return CreatePrimaryUserResponse{ + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError: &struct { + PrimaryUserId string + Description string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + Description: resp["description"].(string), + }, + }, nil + } + } + + linkAccounts := func(recipeUserId RecipeUserID, primaryUserId string, userContext UserContext) (LinkAccountResponse, error) { + requestBody := map[string]interface{}{ + "recipeUserId": recipeUserId.GetAsString(), + "primaryUserId": primaryUserId, + } + resp, err := querier.SendPostRequest("/recipe/accountlinking/user/link", requestBody, userContext) + + if err != nil { + return LinkAccountResponse{}, err + } + + if resp["status"].(string) == "OK" { + var user = User{} + temporaryVariable, err := json.Marshal(resp["user"]) + if err != nil { + return LinkAccountResponse{}, err + } + err = json.Unmarshal(temporaryVariable, &user) + if err != nil { + return LinkAccountResponse{}, err + } + response := LinkAccountResponse{ + OK: &struct { + AccountsAlreadyLinked bool + User User + }{ + AccountsAlreadyLinked: resp["accountsAlreadyLinked"].(bool), + User: user, + }, + } + + verifyEmailForRecipeUserIfLinkedAccountsAreVerified(user, recipeUserId, userContext) + + updatedUser, err := GetUser(user.ID, userContext) + if err != nil { + return LinkAccountResponse{}, err + } + if updatedUser == nil { + return LinkAccountResponse{}, errors.New("this should never be thrown") + } + response.OK.User = *updatedUser + var loginMethod *LoginMethods = nil + for _, method := range response.OK.User.LoginMethods { + if method.RecipeUserID.GetAsString() == recipeUserId.GetAsString() { + loginMethod = &method + break + } + } + + if loginMethod == nil { + return LinkAccountResponse{}, errors.New("this should never be thrown") + } + + err = config.OnAccountLinked(response.OK.User, loginMethod.RecipeLevelUser, userContext) + if err != nil { + return LinkAccountResponse{}, err + } + + return response, nil + } else if resp["status"].(string) == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" { + var user = User{} + temporaryVariable, err := json.Marshal(resp["user"]) + if err != nil { + return LinkAccountResponse{}, err + } + err = json.Unmarshal(temporaryVariable, &user) + if err != nil { + return LinkAccountResponse{}, err + } + + return LinkAccountResponse{ + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError: &struct { + PrimaryUserId string + User User + }{ + PrimaryUserId: resp["primaryUserId"].(string), + User: user, + }, + }, nil + } else if resp["status"].(string) == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" { + return LinkAccountResponse{ + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError: &struct { + PrimaryUserId string + Description string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + Description: resp["description"].(string), + }, + }, nil + } else { + return LinkAccountResponse{ + InputUserIsNotAPrimaryUserError: &struct{}{}, + }, nil + } + } + + canLinkAccounts := func(recipeUserId RecipeUserID, primaryUserId string, userContext UserContext) (CanLinkAccountResponse, error) { + requestBody := map[string]string{ + "recipeUserId": recipeUserId.GetAsString(), + "primaryUserId": primaryUserId, + } + resp, err := querier.SendGetRequest("/recipe/accountlinking/user/link/check", requestBody, userContext) + + if err != nil { + return CanLinkAccountResponse{}, err + } + + if resp["status"].(string) == "OK" { + return CanLinkAccountResponse{ + OK: &struct{ AccountsAlreadyLinked bool }{ + AccountsAlreadyLinked: resp["accountsAlreadyLinked"].(bool), + }, + }, nil + } else if resp["status"].(string) == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" { + return CanLinkAccountResponse{ + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError: &struct { + PrimaryUserId string + Description string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + Description: resp["description"].(string), + }, + }, nil + } else if resp["status"].(string) == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" { + return CanLinkAccountResponse{ + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError: &struct { + PrimaryUserId string + Description string + }{ + PrimaryUserId: resp["primaryUserId"].(string), + Description: resp["description"].(string), + }, + }, nil + } else { + return CanLinkAccountResponse{ + InputUserIsNotAPrimaryUserError: &struct{}{}, + }, nil + } + } + + unlinkAccounts := func(recipeUserId RecipeUserID, userContext UserContext) (UnlinkAccountsResponse, error) { + requestBody := map[string]interface{}{ + "recipeUserId": recipeUserId.GetAsString(), + } + resp, err := querier.SendPostRequest("/recipe/accountlinking/user/unlink", requestBody, userContext) + if err != nil { + return UnlinkAccountsResponse{}, err + } + return UnlinkAccountsResponse{ + WasRecipeUserDeleted: resp["wasRecipeUserDeleted"].(bool), + WasLinked: resp["wasLinked"].(bool), + }, nil + } + + deleteUser := func(userId string, removeAllLinkedAccounts bool, userContext UserContext) error { + requestBody := map[string]interface{}{ + "userId": userId, + "removeAllLinkedAccounts": removeAllLinkedAccounts, + } + _, err := querier.SendPostRequest("/user/remove", requestBody, userContext) + return err + } + + listUserByAccountInfo := func(tenantID string, accountInfo AccountInfo, doUnionOfAccountInfo bool, userContext UserContext) ([]User, error) { + requestBody := map[string]string{ + "doUnionOfAccountInfo": strconv.FormatBool(doUnionOfAccountInfo), + } + + if accountInfo.Email != nil { + requestBody["email"] = *accountInfo.Email + } + if accountInfo.PhoneNumber != nil { + requestBody["phoneNumber"] = *accountInfo.PhoneNumber + } + if accountInfo.ThirdParty != nil { + requestBody["thirdPartyId"] = accountInfo.ThirdParty.ID + requestBody["thirdPartyUserId"] = accountInfo.ThirdParty.UserID + } + + resp, err := querier.SendGetRequest(tenantID+"/users/by-accountinfo", requestBody, userContext) + if err != nil { + return []User{}, err + } + + temporaryVariable, err := json.Marshal(resp["users"]) + if err != nil { + return []User{}, err + } + + var result = []User{} + + err = json.Unmarshal(temporaryVariable, &result) + + if err != nil { + return []User{}, err + } + + return result, nil + } + + return AccountLinkingRecipeInterface{ + GetUsersWithSearchParams: &getUsers, + GetUser: &getUser, + CanCreatePrimaryUser: &canCreatePrimaryUser, + CreatePrimaryUser: &createPrimaryUser, + LinkAccounts: &linkAccounts, + CanLinkAccounts: &canLinkAccounts, + UnlinkAccounts: &unlinkAccounts, + DeleteUser: &deleteUser, + ListUsersByAccountInfo: &listUserByAccountInfo, + } +} diff --git a/supertokens/accountlinkingRecipeInterface.go b/supertokens/accountlinkingRecipeInterface.go new file mode 100644 index 00000000..937d1709 --- /dev/null +++ b/supertokens/accountlinkingRecipeInterface.go @@ -0,0 +1,105 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package supertokens + +type UserPaginationResult struct { + Users []User + NextPaginationToken *string +} + +type CanCreatePrimaryUserResponse struct { + OK *struct { + WasAlreadyAPrimaryUser bool + } + RecipeUserIdAlreadyLinkedWithPrimaryUserIdError *struct { + PrimaryUserId string + Description string + } + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError *struct { + PrimaryUserId string + Description string + } +} + +type CreatePrimaryUserResponse struct { + OK *struct { + User User + WasAlreadyAPrimaryUser bool + } + RecipeUserIdAlreadyLinkedWithPrimaryUserIdError *struct { + PrimaryUserId string + } + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError *struct { + PrimaryUserId string + Description string + } +} + +type CanLinkAccountResponse struct { + OK *struct { + AccountsAlreadyLinked bool + } + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError *struct { + PrimaryUserId string + Description string + } + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError *struct { + PrimaryUserId string + Description string + } + InputUserIsNotAPrimaryUserError *struct{} +} + +type LinkAccountResponse struct { + OK *struct { + AccountsAlreadyLinked bool + User User + } + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdError *struct { + PrimaryUserId string + User User + } + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdError *struct { + PrimaryUserId string + Description string + } + InputUserIsNotAPrimaryUserError *struct{} +} + +type UnlinkAccountsResponse struct { + WasRecipeUserDeleted bool + WasLinked bool +} + +type AccountLinkingRecipeInterface struct { + GetUsersWithSearchParams *func(tenantID string, timeJoinedOrder string, paginationToken *string, limit *int, includeRecipeIds *[]string, searchParams map[string]string, userContext UserContext) (UserPaginationResult, error) + + CanCreatePrimaryUser *func(recipeUserId RecipeUserID, userContext UserContext) (CanCreatePrimaryUserResponse, error) + + CreatePrimaryUser *func(recipeUserId RecipeUserID, userContext UserContext) (CreatePrimaryUserResponse, error) + + CanLinkAccounts *func(recipeUserId RecipeUserID, primaryUserId string, userContext UserContext) (CanLinkAccountResponse, error) + + LinkAccounts *func(recipeUserId RecipeUserID, primaryUserId string, userContext UserContext) (LinkAccountResponse, error) + + UnlinkAccounts *func(recipeUserId RecipeUserID, userContext UserContext) (UnlinkAccountsResponse, error) + + GetUser *func(userId string, userContext UserContext) (*User, error) + + ListUsersByAccountInfo *func(tenantID string, accountInfo AccountInfo, doUnionOfAccountInfo bool, userContext UserContext) ([]User, error) + + DeleteUser *func(userId string, removeAllLinkedAccounts bool, userContext UserContext) error +} diff --git a/supertokens/accountlinkingUtils.go b/supertokens/accountlinkingUtils.go new file mode 100644 index 00000000..992bf9ad --- /dev/null +++ b/supertokens/accountlinkingUtils.go @@ -0,0 +1,56 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package supertokens + +func validateAndNormaliseAccountLinkingUserInput(appInfo NormalisedAppinfo, config *AccountLinkingTypeInput) AccountLinkingTypeNormalisedInput { + + typeNormalisedInput := makeTypeNormalisedInput(appInfo) + + if config != nil && config.ShouldDoAutomaticAccountLinking != nil { + typeNormalisedInput.ShouldDoAutomaticAccountLinking = config.ShouldDoAutomaticAccountLinking + } + + if config != nil && config.OnAccountLinked != nil { + typeNormalisedInput.OnAccountLinked = config.OnAccountLinked + } + + if config != nil && config.Override != nil { + if config.Override.Functions != nil { + typeNormalisedInput.Override.Functions = config.Override.Functions + } + } + + return typeNormalisedInput +} + +func makeTypeNormalisedInput(appInfo NormalisedAppinfo) AccountLinkingTypeNormalisedInput { + return AccountLinkingTypeNormalisedInput{ + OnAccountLinked: func(user User, newAccountUser RecipeLevelUser, userContext UserContext) error { + return nil + }, + ShouldDoAutomaticAccountLinking: func(newAccountInfo AccountInfoWithRecipeIdAndWithRecipeUserId, user *User, tenantID string, userContext UserContext) (ShouldDoAutomaticAccountLinkingResponse, error) { + return ShouldDoAutomaticAccountLinkingResponse{ + ShouldAutomaticallyLink: false, + ShouldRequireVerification: false, + }, nil + }, + Override: AccountLinkingOverrideStruct{ + Functions: func(originalImplementation AccountLinkingRecipeInterface) AccountLinkingRecipeInterface { + return originalImplementation + }, + }, + } +} diff --git a/supertokens/alwaysInitialisedRecipes.go b/supertokens/alwaysInitialisedRecipes.go index cfb756b3..c6050456 100644 --- a/supertokens/alwaysInitialisedRecipes.go +++ b/supertokens/alwaysInitialisedRecipes.go @@ -1,3 +1,3 @@ package supertokens -var DefaultMultitenancyRecipe Recipe +var InternalUseDefaultMultitenancyRecipe Recipe diff --git a/supertokens/constants.go b/supertokens/constants.go index 01ba16c5..61e5d9d1 100644 --- a/supertokens/constants.go +++ b/supertokens/constants.go @@ -21,13 +21,13 @@ const ( ) // VERSION current version of the lib -const VERSION = "0.17.3" +const VERSION = "0.18.0" var ( - cdiSupported = []string{"3.0"} + cdiSupported = []string{"4.0"} ) -const DashboardVersion = "0.7" +const DashboardVersion = "0.8" const DefaultTenantId string = "public" diff --git a/supertokens/emailverificationProxy.go b/supertokens/emailverificationProxy.go new file mode 100644 index 00000000..ba668451 --- /dev/null +++ b/supertokens/emailverificationProxy.go @@ -0,0 +1,42 @@ +package supertokens + +// This is there so that the supertokens package can use the email verification +// recipe since account linking requires it. We cannot directly use the ev recipe from here +// cause of cyclic dependencies. + +var InternalUseEmailVerificationRecipeProxyInstance *InternalUseEmailVerificationRecipeProxy = nil + +type InternalUseEmailVerificationRecipeProxy struct { + CreateEmailVerificationToken func(recipeUserID RecipeUserID, email string, tenantId string, userContext UserContext) (InternalUseCreateEmailVerificationTokenResponse, error) + VerifyEmailUsingToken func(token string, tenantId string, attemptAccountLinking bool, userContext UserContext) (InternalUseVerifyEmailUsingTokenResponse, error) + IsEmailVerified func(userID, email string, userContext UserContext) (bool, error) + RevokeEmailVerificationTokens func(userId, email string, tenantId string, userContext UserContext) (InternalUseRevokeEmailVerificationTokensResponse, error) + UnverifyEmail func(userId, email string, userContext UserContext) (InternalUseUnverifyEmailResponse, error) +} + +type InternalUseCreateEmailVerificationTokenResponse struct { + OK *struct { + Token string + } + EmailAlreadyVerifiedError *struct{} +} + +type InternalUseVerifyEmailUsingTokenResponse struct { + OK *struct { + User InternalUseEmailVerificationUser + } + EmailVerificationInvalidTokenError *struct{} +} + +type InternalUseRevokeEmailVerificationTokensResponse struct { + OK *struct{} +} + +type InternalUseUnverifyEmailResponse struct { + OK *struct{} +} + +type InternalUseEmailVerificationUser struct { + ID string `json:"id"` + Email string `json:"email"` +} diff --git a/supertokens/main.go b/supertokens/main.go index df5a8a8b..4f08125c 100644 --- a/supertokens/main.go +++ b/supertokens/main.go @@ -31,6 +31,10 @@ func Init(config TypeInput) error { return nil } +func InitAccountLinking(config *AccountLinkingTypeInput) Recipe { + return accountLinkingRecipeInit(config) +} + func Middleware(theirHandler http.Handler) http.Handler { instance, err := GetInstanceOrThrowError() if err != nil { @@ -71,18 +75,136 @@ func GetUserCount(includeRecipeIds *[]string, tenantId *string) (float64, error) return getUserCount(includeRecipeIds, *tenantId, includeAllTenants) } -func GetUsersOldestFirst(tenantId string, paginationToken *string, limit *int, includeRecipeIds *[]string, query map[string]string) (UserPaginationResult, error) { - return GetUsersWithSearchParams(tenantId, "ASC", paginationToken, limit, includeRecipeIds, query) +func GetUsersOldestFirst(tenantId string, paginationToken *string, limit *int, includeRecipeIds *[]string, query map[string]string, userContext ...UserContext) (UserPaginationResult, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return UserPaginationResult{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.GetUsersWithSearchParams)(tenantId, "ASC", paginationToken, limit, includeRecipeIds, query, userContext[0]) +} + +func GetUsersNewestFirst(tenantId string, paginationToken *string, limit *int, includeRecipeIds *[]string, query map[string]string, userContext ...UserContext) (UserPaginationResult, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return UserPaginationResult{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.GetUsersWithSearchParams)(tenantId, "DESC", paginationToken, limit, includeRecipeIds, query, userContext[0]) } -func GetUsersNewestFirst(tenantId string, paginationToken *string, limit *int, includeRecipeIds *[]string, query map[string]string) (UserPaginationResult, error) { - return GetUsersWithSearchParams(tenantId, "DESC", paginationToken, limit, includeRecipeIds, query) +func GetUser(userId string, userContext ...UserContext) (*User, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return nil, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.GetUser)(userId, userContext[0]) } -func DeleteUser(userId string) error { - return deleteUser(userId) +func DeleteUser(userId string, removeAllLinkedAccounts bool, userContext ...UserContext) error { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.DeleteUser)(userId, removeAllLinkedAccounts, userContext[0]) } func GetRequestFromUserContext(userContext UserContext) *http.Request { return getRequestFromUserContext(userContext) } + +func CanCreatePrimaryUser(recipeUserId RecipeUserID, userContext ...UserContext) (CanCreatePrimaryUserResponse, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return CanCreatePrimaryUserResponse{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.CanCreatePrimaryUser)(recipeUserId, userContext[0]) +} + +func CreatePrimaryUser(recipeUserId RecipeUserID, userContext ...UserContext) (CreatePrimaryUserResponse, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return CreatePrimaryUserResponse{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.CreatePrimaryUser)(recipeUserId, userContext[0]) +} + +func LinkAccounts(recipeUserId RecipeUserID, primaryUserId string, userContext ...UserContext) (LinkAccountResponse, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return LinkAccountResponse{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.LinkAccounts)(recipeUserId, primaryUserId, userContext[0]) +} + +func CanLinkAccounts(recipeUserId RecipeUserID, primaryUserId string, userContext ...UserContext) (CanLinkAccountResponse, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return CanLinkAccountResponse{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.CanLinkAccounts)(recipeUserId, primaryUserId, userContext[0]) +} + +func UnlinkAccounts(recipeUserId RecipeUserID, userContext ...UserContext) (UnlinkAccountsResponse, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return UnlinkAccountsResponse{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.UnlinkAccounts)(recipeUserId, userContext[0]) +} + +func ListUsersByAccountInfo(tenantID string, accountInfo AccountInfo, doUnionOfAccountInfo bool, userContext ...UserContext) ([]User, error) { + accountLinkingInstance, err := getAccountLinkingRecipeInstanceOrThrowError() + if err != nil { + return []User{}, err + } + + if len(userContext) == 0 { + userContext = append(userContext, &map[string]interface{}{}) + } + + return (*accountLinkingInstance.RecipeImpl.ListUsersByAccountInfo)(tenantID, accountInfo, doUnionOfAccountInfo, userContext[0]) +} diff --git a/supertokens/models.go b/supertokens/models.go index 7f84792e..33e9275b 100644 --- a/supertokens/models.go +++ b/supertokens/models.go @@ -17,6 +17,9 @@ package supertokens import ( "net/http" + "strings" + + "github.com/nyaruka/phonenumbers" ) type NormalisedAppinfo struct { @@ -70,3 +73,109 @@ type UserContext = *map[string]interface{} type GeneralErrorResponse struct { Message string } +type ThirdParty struct { + ID string `json:"id"` + UserID string `json:"userId"` +} + +type RecipeID string + +const ( + EmailPasswordRID RecipeID = "emailpassword" + ThirdPartyRID RecipeID = "thirdparty" + PasswordlessRID RecipeID = "passwordless" +) + +type LoginMethods struct { + RecipeLevelUser + Verified bool `json:"verified"` +} + +func (r *LoginMethods) HasSameEmailAs(email *string) bool { + if email == nil { + return false + } + trimmedEmail := strings.ToLower(strings.TrimSpace(*email)) + return r.Email != nil && *r.Email == trimmedEmail +} + +func (r *LoginMethods) HasSamePhoneNumberAs(phoneNumber *string) bool { + if phoneNumber == nil { + return false + } + trimmedPhoneNumber := strings.TrimSpace(*phoneNumber) + parsedPhoneNumber, err := phonenumbers.Parse(trimmedPhoneNumber, "") + formattedPhoneNumber := strings.TrimSpace(trimmedPhoneNumber) + + if err == nil { + // we do not have an else statement cause in that case, we just want to trim, + // which is already happening above. + formattedPhoneNumber = phonenumbers.Format(parsedPhoneNumber, phonenumbers.E164) + } + + return r.PhoneNumber != nil && *r.PhoneNumber == formattedPhoneNumber +} + +func (r *LoginMethods) HasSameThirdPartyInfoAs(thirdParty *ThirdParty) bool { + if thirdParty == nil { + return false + } + thirdPartyId := strings.TrimSpace(thirdParty.ID) + thirdPartyUserID := strings.TrimSpace(thirdParty.UserID) + return r.ThirdParty != nil && r.ThirdParty.ID == thirdPartyId && r.ThirdParty.UserID == thirdPartyUserID +} + +type User struct { + ID string `json:"id"` + TimeJoined uint64 `json:"timeJoined"` + IsPrimaryUser bool `json:"isPrimaryUser"` + TenantIDs []string `json:"tenantIds"` + Emails []string `json:"emails"` + PhoneNumbers []string `json:"phoneNumbers"` + ThirdParty []ThirdParty `json:"thirdParty"` + LoginMethods []LoginMethods `json:"loginMethods"` +} + +type AccountInfo struct { + Email *string `json:"email,omitempty"` + PhoneNumber *string `json:"phoneNumber,omitempty"` + ThirdParty *ThirdParty `json:"thirdParty,omitempty"` +} + +type AccountInfoWithRecipeID struct { + RecipeID RecipeID `json:"recipeId"` + AccountInfo +} + +type RecipeLevelUser struct { + TenantIDs []string `json:"tenantIds"` + TimeJoined uint64 `json:"timeJoined"` + RecipeUserID RecipeUserID `json:"recipeUserId"` + AccountInfoWithRecipeID +} + +type AccountInfoWithRecipeIdAndWithRecipeUserId struct { + RecipeUserId *RecipeUserID + AccountInfoWithRecipeID +} + +type ShouldDoAutomaticAccountLinkingResponse struct { + ShouldAutomaticallyLink bool + ShouldRequireVerification bool +} + +type AccountLinkingTypeInput struct { + OnAccountLinked func(user User, newAccountUser RecipeLevelUser, userContext UserContext) error + ShouldDoAutomaticAccountLinking func(newAccountInfo AccountInfoWithRecipeIdAndWithRecipeUserId, user *User, tenantID string, userContext UserContext) (ShouldDoAutomaticAccountLinkingResponse, error) + Override *AccountLinkingOverrideStruct +} + +type AccountLinkingTypeNormalisedInput struct { + OnAccountLinked func(user User, newAccountUser RecipeLevelUser, userContext UserContext) error + ShouldDoAutomaticAccountLinking func(newAccountInfo AccountInfoWithRecipeIdAndWithRecipeUserId, user *User, tenantID string, userContext UserContext) (ShouldDoAutomaticAccountLinkingResponse, error) + Override AccountLinkingOverrideStruct +} + +type AccountLinkingOverrideStruct struct { + Functions func(originalImplementation AccountLinkingRecipeInterface) AccountLinkingRecipeInterface +} diff --git a/supertokens/models_test.go b/supertokens/models_test.go new file mode 100644 index 00000000..8572a1fb --- /dev/null +++ b/supertokens/models_test.go @@ -0,0 +1,350 @@ +package supertokens + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUserJSONStruct(t *testing.T) { + recipeUserId1, _ := NewRecipeUserID("123") + recipeUserId2, _ := NewRecipeUserID("haha") + email1 := "e1" + phoneNumber1 := "p1" + thirdParty1 := ThirdParty{ + ID: "tp1", + UserID: "tpu1", + } + email2 := "e2" + phoneNumber2 := "p2" + thirdParty2 := ThirdParty{ + ID: "tp2", + UserID: "tpu2", + } + + user := User{ + ID: "123", + TimeJoined: 123, + IsPrimaryUser: true, + TenantIDs: []string{"t1", "t2"}, + Emails: []string{"e1", "e2"}, + PhoneNumbers: []string{"p1", "p2"}, + ThirdParty: []ThirdParty{ + { + ID: "tp1", + UserID: "tpu1", + }, + { + ID: "tp2", + UserID: "tpu2", + }, + }, + LoginMethods: []LoginMethods{ + { + Verified: true, + RecipeLevelUser: RecipeLevelUser{ + TenantIDs: []string{"t1", "t2"}, + TimeJoined: 123, + RecipeUserID: recipeUserId1, + AccountInfoWithRecipeID: AccountInfoWithRecipeID{ + RecipeID: EmailPasswordRID, + AccountInfo: AccountInfo{ + Email: &email1, + PhoneNumber: &phoneNumber1, + ThirdParty: &thirdParty1, + }, + }, + }, + }, + { + Verified: true, + RecipeLevelUser: RecipeLevelUser{ + TenantIDs: []string{"t1", "t2"}, + TimeJoined: 123, + RecipeUserID: recipeUserId2, + AccountInfoWithRecipeID: AccountInfoWithRecipeID{ + RecipeID: EmailPasswordRID, + AccountInfo: AccountInfo{ + Email: &email2, + PhoneNumber: &phoneNumber2, + ThirdParty: &thirdParty2, + }, + }, + }, + }, + }, + } + + jsonified, err := json.Marshal(user) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, "{\"id\":\"123\",\"timeJoined\":123,\"isPrimaryUser\":true,\"tenantIds\":[\"t1\",\"t2\"],\"emails\":[\"e1\",\"e2\"],\"phoneNumbers\":[\"p1\",\"p2\"],\"thirdParty\":[{\"id\":\"tp1\",\"userId\":\"tpu1\"},{\"id\":\"tp2\",\"userId\":\"tpu2\"}],\"loginMethods\":[{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"123\",\"recipeId\":\"emailpassword\",\"email\":\"e1\",\"phoneNumber\":\"p1\",\"thirdParty\":{\"id\":\"tp1\",\"userId\":\"tpu1\"},\"verified\":true},{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"haha\",\"recipeId\":\"emailpassword\",\"email\":\"e2\",\"phoneNumber\":\"p2\",\"thirdParty\":{\"id\":\"tp2\",\"userId\":\"tpu2\"},\"verified\":true}]}", string(jsonified)) + + // now we test unmarshaling + var user2 User + err = json.Unmarshal(jsonified, &user2) + if err != nil { + t.Error(err) + return + } + + // check that user2 and user are the same + assert.Equal(t, user, user2) + + // we marshall it once again + jsonified, err = json.Marshal(user2) + if err != nil { + t.Error(err) + return + } + assert.Equal(t, "{\"id\":\"123\",\"timeJoined\":123,\"isPrimaryUser\":true,\"tenantIds\":[\"t1\",\"t2\"],\"emails\":[\"e1\",\"e2\"],\"phoneNumbers\":[\"p1\",\"p2\"],\"thirdParty\":[{\"id\":\"tp1\",\"userId\":\"tpu1\"},{\"id\":\"tp2\",\"userId\":\"tpu2\"}],\"loginMethods\":[{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"123\",\"recipeId\":\"emailpassword\",\"email\":\"e1\",\"phoneNumber\":\"p1\",\"thirdParty\":{\"id\":\"tp1\",\"userId\":\"tpu1\"},\"verified\":true},{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"haha\",\"recipeId\":\"emailpassword\",\"email\":\"e2\",\"phoneNumber\":\"p2\",\"thirdParty\":{\"id\":\"tp2\",\"userId\":\"tpu2\"},\"verified\":true}]}", string(jsonified)) +} + +func TestUserJSONStructWithMissingAccountInfo(t *testing.T) { + recipeUserId1, _ := NewRecipeUserID("123") + recipeUserId2, _ := NewRecipeUserID("456") + user := User{ + ID: "123", + TimeJoined: 123, + IsPrimaryUser: true, + TenantIDs: []string{}, + Emails: []string{}, + PhoneNumbers: []string{}, + ThirdParty: []ThirdParty{}, + LoginMethods: []LoginMethods{ + { + Verified: true, + RecipeLevelUser: RecipeLevelUser{ + TenantIDs: []string{"t1", "t2"}, + TimeJoined: 123, + RecipeUserID: recipeUserId1, + AccountInfoWithRecipeID: AccountInfoWithRecipeID{ + RecipeID: EmailPasswordRID, + AccountInfo: AccountInfo{ + Email: nil, + }, + }, + }, + }, + { + Verified: true, + RecipeLevelUser: RecipeLevelUser{ + TenantIDs: []string{"t1", "t2"}, + TimeJoined: 123, + RecipeUserID: recipeUserId2, + AccountInfoWithRecipeID: AccountInfoWithRecipeID{ + RecipeID: EmailPasswordRID, + AccountInfo: AccountInfo{ + PhoneNumber: nil, + }, + }, + }, + }, + }, + } + + jsonified, err := json.Marshal(user) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, "{\"id\":\"123\",\"timeJoined\":123,\"isPrimaryUser\":true,\"tenantIds\":[],\"emails\":[],\"phoneNumbers\":[],\"thirdParty\":[],\"loginMethods\":[{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"123\",\"recipeId\":\"emailpassword\",\"verified\":true},{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"456\",\"recipeId\":\"emailpassword\",\"verified\":true}]}", string(jsonified)) + + // now we test unmarshaling + var user2 User + err = json.Unmarshal(jsonified, &user2) + if err != nil { + t.Error(err) + return + } + + // check that user2 and user are the same + assert.Equal(t, user, user2) + + // we marshall it once again + jsonified, err = json.Marshal(user2) + if err != nil { + t.Error(err) + return + } + assert.Equal(t, "{\"id\":\"123\",\"timeJoined\":123,\"isPrimaryUser\":true,\"tenantIds\":[],\"emails\":[],\"phoneNumbers\":[],\"thirdParty\":[],\"loginMethods\":[{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"123\",\"recipeId\":\"emailpassword\",\"verified\":true},{\"tenantIds\":[\"t1\",\"t2\"],\"timeJoined\":123,\"recipeUserId\":\"456\",\"recipeId\":\"emailpassword\",\"verified\":true}]}", string(jsonified)) +} + +func TestHelperFunctions(t *testing.T) { + recipeUserId1, _ := NewRecipeUserID("123") + recipeUserId2, _ := NewRecipeUserID("haha") + email1 := "e1" + phoneNumber1 := "+36701234123" + thirdParty1 := ThirdParty{ + ID: "tp1", + UserID: "tpu1", + } + + user := User{ + ID: "123", + TimeJoined: 123, + IsPrimaryUser: true, + TenantIDs: []string{"t1", "t2"}, + Emails: []string{"e1", "e2"}, + PhoneNumbers: []string{"p1", "p2"}, + ThirdParty: []ThirdParty{ + { + ID: "tp1", + UserID: "tpu1", + }, + { + ID: "tp2", + UserID: "tpu2", + }, + }, + LoginMethods: []LoginMethods{ + { + Verified: true, + RecipeLevelUser: RecipeLevelUser{ + TenantIDs: []string{"t1", "t2"}, + TimeJoined: 123, + RecipeUserID: recipeUserId1, + AccountInfoWithRecipeID: AccountInfoWithRecipeID{ + RecipeID: EmailPasswordRID, + AccountInfo: AccountInfo{ + Email: &email1, + PhoneNumber: &phoneNumber1, + ThirdParty: &thirdParty1, + }, + }, + }, + }, + { + Verified: true, + RecipeLevelUser: RecipeLevelUser{ + TenantIDs: []string{"t1", "t2"}, + TimeJoined: 123, + RecipeUserID: recipeUserId2, + AccountInfoWithRecipeID: AccountInfoWithRecipeID{ + RecipeID: EmailPasswordRID, + AccountInfo: AccountInfo{ + Email: nil, + PhoneNumber: nil, + ThirdParty: nil, + }, + }, + }, + }, + }, + } + + { + email := "e1" + assert.True(t, user.LoginMethods[0].HasSameEmailAs(&email)) + } + { + email := "E1" + assert.True(t, user.LoginMethods[0].HasSameEmailAs(&email)) + } + { + email := "E1 " + assert.True(t, user.LoginMethods[0].HasSameEmailAs(&email)) + } + { + email := "e" + assert.False(t, user.LoginMethods[0].HasSameEmailAs(&email)) + assert.False(t, user.LoginMethods[0].HasSameEmailAs(nil)) + } + { + email := "e1" + assert.False(t, user.LoginMethods[1].HasSameEmailAs(&email)) + assert.False(t, user.LoginMethods[1].HasSameEmailAs(nil)) + } + + { + phoneNumber := "+36701234123" + assert.True(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + } + { + phoneNumber := " \t+36-70/1234 123 " + assert.True(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + } + { + phoneNumber := " \t+36701234123 " + assert.True(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + } + { + phoneNumber := "36701234123" + assert.False(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + } + { + phoneNumber := "0036701234123" + assert.False(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + } + { + phoneNumber := "06701234123" + assert.False(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + } + { + phoneNumber := "p36701234123" + assert.False(t, user.LoginMethods[0].HasSamePhoneNumberAs(&phoneNumber)) + assert.False(t, user.LoginMethods[0].HasSamePhoneNumberAs(nil)) + } + { + phoneNumber := "+36701234123" + assert.False(t, user.LoginMethods[1].HasSamePhoneNumberAs(&phoneNumber)) + assert.False(t, user.LoginMethods[1].HasSamePhoneNumberAs(nil)) + } + { + thirdParty := ThirdParty{ + ID: "tp1", + UserID: "tpu1", + } + assert.True(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(&thirdParty)) + } + { + thirdParty := ThirdParty{ + ID: " tp1 ", + UserID: " tpu1\t", + } + assert.True(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(&thirdParty)) + } + { + thirdParty := ThirdParty{ + ID: " tp1 ", + UserID: " Tpu1\t", + } + assert.False(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(&thirdParty)) + } + + { + thirdParty := ThirdParty{ + ID: " Tp1 ", + UserID: " tpu1\t", + } + assert.False(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(&thirdParty)) + } + { + thirdParty := ThirdParty{ + ID: " tp12 ", + UserID: " tpU1\t", + } + assert.False(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(&thirdParty)) + } + { + thirdParty := ThirdParty{ + ID: " tp1 ", + UserID: " tpU2\t", + } + assert.False(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(&thirdParty)) + assert.False(t, user.LoginMethods[0].HasSameThirdPartyInfoAs(nil)) + } + { + thirdParty := ThirdParty{ + ID: "tp1", + UserID: "tpu1", + } + assert.False(t, user.LoginMethods[1].HasSameThirdPartyInfoAs(&thirdParty)) + assert.False(t, user.LoginMethods[1].HasSameThirdPartyInfoAs(nil)) + } +} diff --git a/supertokens/recipeUserId.go b/supertokens/recipeUserId.go new file mode 100644 index 00000000..292848f4 --- /dev/null +++ b/supertokens/recipeUserId.go @@ -0,0 +1,42 @@ +package supertokens + +import ( + "encoding/json" + "errors" +) + +type RecipeUserID struct { + recipeUserID string +} + +func NewRecipeUserID(recipeUserID string) (RecipeUserID, error) { + if recipeUserID == "" { + return RecipeUserID{}, errors.New("recipeUserID cannot be empty") + } + return RecipeUserID{recipeUserID: recipeUserID}, nil +} + +func (r *RecipeUserID) GetAsString() string { + return r.recipeUserID +} + +func (r *RecipeUserID) MarshalJSON() ([]byte, error) { + // convert r.recipeUserId to string and return that + return json.Marshal(r.recipeUserID) +} + +// add custom unmarshal function +func (r *RecipeUserID) UnmarshalJSON(data []byte) error { + + // unmarshal to a string + var recipeUserID string + err := json.Unmarshal(data, &recipeUserID) + if err != nil { + return err + } + + // set r.recipeUserId to the string + r.recipeUserID = recipeUserID + + return nil +} diff --git a/supertokens/supertokens.go b/supertokens/supertokens.go index b3bc166e..89fd0732 100644 --- a/supertokens/supertokens.go +++ b/supertokens/supertokens.go @@ -112,6 +112,7 @@ func supertokensInit(config TypeInput) error { } multitenancyFound := false + accountLinkingFound := false for _, elem := range config.RecipeList { recipeModule, err := elem(superTokens.AppInfo, superTokens.OnSuperTokensAPIError) @@ -123,10 +124,21 @@ func supertokensInit(config TypeInput) error { if recipeModule.GetRecipeID() == "multitenancy" { multitenancyFound = true } + if recipeModule.GetRecipeID() == "accountlinking" { + accountLinkingFound = true + } + } + + if !multitenancyFound && InternalUseDefaultMultitenancyRecipe != nil { + recipeModule, err := InternalUseDefaultMultitenancyRecipe(superTokens.AppInfo, superTokens.OnSuperTokensAPIError) + if err != nil { + return err + } + superTokens.RecipeModules = append(superTokens.RecipeModules, *recipeModule) } - if !multitenancyFound && DefaultMultitenancyRecipe != nil { - recipeModule, err := DefaultMultitenancyRecipe(superTokens.AppInfo, superTokens.OnSuperTokensAPIError) + if !accountLinkingFound { + recipeModule, err := accountLinkingRecipeInit(nil)(superTokens.AppInfo, superTokens.OnSuperTokensAPIError) if err != nil { return err } @@ -311,60 +323,6 @@ func (s *superTokens) errorHandler(originalError error, req *http.Request, res h return originalError } -type UserPaginationResult struct { - Users []struct { - RecipeId string `json:"recipeId"` - User map[string]interface{} `json:"user"` - } - NextPaginationToken *string -} - -// TODO: Add tests -func GetUsersWithSearchParams(tenantId string, timeJoinedOrder string, paginationToken *string, limit *int, includeRecipeIds *[]string, searchParams map[string]string) (UserPaginationResult, error) { - - querier, err := GetNewQuerierInstanceOrThrowError("") - if err != nil { - return UserPaginationResult{}, err - } - - requestBody := map[string]string{} - if searchParams != nil { - requestBody = searchParams - } - requestBody["timeJoinedOrder"] = timeJoinedOrder - - if limit != nil { - requestBody["limit"] = strconv.Itoa(*limit) - } - if paginationToken != nil { - requestBody["paginationToken"] = *paginationToken - } - if includeRecipeIds != nil { - requestBody["includeRecipeIds"] = strings.Join((*includeRecipeIds)[:], ",") - } - - resp, err := querier.SendGetRequest(tenantId+"/users", requestBody, nil) - - if err != nil { - return UserPaginationResult{}, err - } - - temporaryVariable, err := json.Marshal(resp) - if err != nil { - return UserPaginationResult{}, err - } - - var result = UserPaginationResult{} - - err = json.Unmarshal(temporaryVariable, &result) - - if err != nil { - return UserPaginationResult{}, err - } - - return result, nil -} - // TODO: Add tests func getUserCount(includeRecipeIds *[]string, tenantId string, includeAllTenants *bool) (float64, error) { @@ -391,35 +349,10 @@ func getUserCount(includeRecipeIds *[]string, tenantId string, includeAllTenants return resp["count"].(float64), nil } -func deleteUser(userId string) error { - querier, err := GetNewQuerierInstanceOrThrowError("") - if err != nil { - return err - } - - cdiVersion, err := querier.GetQuerierAPIVersion() - if err != nil { - return err - } - - if MaxVersion(cdiVersion, "2.10") == cdiVersion { - _, err = querier.SendPostRequest("/user/remove", map[string]interface{}{ - "userId": userId, - }, nil) - - if err != nil { - return err - } - - return nil - } else { - return errors.New("please upgrade the SuperTokens core to >= 3.7.0") - } -} - func ResetForTest() { ResetQuerierForTest() superTokensInstance = nil + singletonInstance = nil } func IsRunningInTestMode() bool { diff --git a/test/auth-react-server/main.go b/test/auth-react-server/main.go index 7f79e2e8..dfc879eb 100644 --- a/test/auth-react-server/main.go +++ b/test/auth-react-server/main.go @@ -803,7 +803,7 @@ func callSTInit(passwordlessConfig *plessmodels.TypeInput) { if err != nil { return } - err = supertokens.DeleteUser(user.ID) + err = supertokens.DeleteUser(user.ID, true) if err != nil { return }