Skip to content

Commit 0319edb

Browse files
account linking changes (#108)
* change to types * implements equals method * cleans up interface * interface change * bug fixes * adds json serialisation for older CDIs * all test fixes * adds common function to get user based on id * new func * removes unnecessary function * removes unused function * adds function for list user by phone number * more intefrace function * removes unused interface func * adds hash code function * makes tenant ids into a set * change of interface * adds functions for getting user info with locking * changes to interface to make it per tenant * renames var * small change * adds link account function * more tests * delete function change * adds function for unlink accounts * enforeces calling of external user ID mapping before converting to json * implements toJson * fixes a bug * fixes bugs * more changes * changes for password reset flow * removes unneeded function * removes unneeded function * fixes a bug * adds recipe user id in session * fix: primary user id in unlink accounts (#109) * fix: externalUserId in LoginMethod (#110) * fix: externalUserId in LoginMethod * fix: pr comments * fix: pr comments * fix: pr comments * fix: External userid (#111) * fix: externalUserId in LoginMethod * fix: pr comments * fix: pr comments * fix: pr comments * fix: external userid * fix: remove UserInfo class (#112) * fix: remove UserInfo class * fix: remove getRecipeId * fix: verified not final in LoginMethod (#113) * fix: multitenancy changes (#114) * Al user tenant association (#115) * fix: multitenancy changes * fix: addUserIdToTenant to transaction * fix: remove con reuse (#116) * fix: tests * fix: tenantIds instance * fix: account linking stats (#117) * fix: functions for useridmapping (#118) * fix: version and changelog --------- Co-authored-by: Sattvik Chakravarthy <[email protected]> Co-authored-by: Sattvik Chakravarthy <[email protected]>
1 parent 4c13b83 commit 0319edb

29 files changed

+654
-235
lines changed

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,52 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
## [4.0.0] - 2023-09-19
11+
12+
- Adds support for account linking
13+
- Adds `AuthRecipeUserInfo` class and removes `UserInfo` from emailpassword, passwordless and thirdparty.
14+
- ActiveUsersStorage interface changes
15+
- Removes `deleteUserActive`
16+
- Adds `deleteUserActive_Transaction`
17+
- Adds `countUsersThatHaveMoreThanOneLoginMethodAndActiveSince`
18+
- EmailPasswordStorage interfaces changes
19+
- Removes `deleteEmailPasswordUser`, `getUserInfoUsingId`, `getUserInfoUsingEmail`
20+
- Changes return type of `signUp` from `UserInfo` to `AuthRecipeUserInfo`
21+
- Changes `PasswordResetTokenInfo` to accept additional param `email`
22+
- EmailPasswordSQLStorage interface changes
23+
- Removes `getUserInfoUsingId_Transaction`
24+
- Adds `deleteEmailPasswordUser_Transaction`
25+
- EmailVerificationStorage interface changes
26+
- Removes `deleteEmailVerificationUserInfo`
27+
- EmailVerificationSQLStorage interface changes
28+
- Adds `deleteEmailVerificationUserInfo_Transaction`
29+
- MultitenancyStorage interface changes
30+
- Removes `addUserIdToTenant`
31+
- MultitenancySQLStorage interface changes
32+
- Adds `addUserIdToTenant_Transaction`
33+
- PasswordlessStorage interface changes
34+
- Changes return type of `createUser` from `UserInfo` to `AuthRecipeUserInfo`
35+
- Removes `deletePasswordlessUser`, `getUserById`, `getUserByEmail`, `getUserByPhoneNumber`
36+
- PasswordlessSQLStorage interface changes
37+
- Adds `deletePasswordlessUser_Transaction`
38+
- SessionInfo accepts additional parameter `recipeUserId`
39+
- SessionSQLStorage interface changes
40+
- Adds `deleteSessionsOfUser_Transaction`
41+
- ThirdPartyStorage interface changes
42+
- Removes `deleteThirdPartyUser`, `getThirdPartyUserInfoUsingId`, `getThirdPartyUserInfoUsingId`, `getThirdPartyUsersByEmail`
43+
- Changes return type of `signUp` from `UserInfo` to `AuthRecipeUserInfo`
44+
- ThirdPartySQLStorage interface changes
45+
- Adds `deleteThirdPartyUser_Transaction`
46+
- Removes `getUserInfoUsingId_Transaction`
47+
- UserIdMappingSQLStorage interface changes
48+
- Adds `getUserIdMapping_Transaction`, `getUserIdMapping_Transaction`
49+
- UserMetadataSQLStorage interface changes
50+
- Adds `deleteUserMetadata_Transaction`
51+
- UserRolesStorage interface changes
52+
- Removes `deleteAllRolesForUser`
53+
- UserRolesSQLStorage interface changes
54+
- Adds `deleteAllRolesForUser_Transaction`
55+
1056
## [3.0.1] - 2023-07-04
1157

1258
- Updates `TenantConfig` toJson function to protect core config as well.

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ plugins {
22
id 'java-library'
33
}
44

5-
version = "3.0.1"
5+
version = "4.0.0"
66

77
repositories {
88
mavenCentral()

src/main/java/io/supertokens/pluginInterface/ActiveUsersStorage.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
44
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
55
import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage;
6+
import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
67

78
public interface ActiveUsersStorage extends NonAuthRecipeStorage {
89
/* Update the last active time of a user to now */
@@ -17,5 +18,8 @@ public interface ActiveUsersStorage extends NonAuthRecipeStorage {
1718
/* Count the number of users who have enabled TOTP and are active */
1819
int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException;
1920

20-
void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException;
21+
void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId)
22+
throws StorageQueryException;
23+
24+
int countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException;
2125
}

src/main/java/io/supertokens/pluginInterface/RECIPE_ID.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public enum RECIPE_ID {
2222
EMAIL_PASSWORD("emailpassword"), THIRD_PARTY("thirdparty"), SESSION("session"),
2323
EMAIL_VERIFICATION("emailverification"), JWT("jwt"), PASSWORDLESS("passwordless"), USER_METADATA("usermetadata"),
2424
USER_ROLES("userroles"), USER_ID_MAPPING("useridmapping"), DASHBOARD("dashboard"), TOTP("totp"),
25-
MULTITENANCY("multitenancy");
25+
MULTITENANCY("multitenancy"), ACCOUNT_LINKING("accountlinking");
2626

2727
private final String name;
2828

src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeStorage.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,24 @@ AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @Nonnull Intege
4444
boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException;
4545

4646
boolean doesUserIdExist(TenantIdentifier tenantIdentifierIdentifier, String userId) throws StorageQueryException;
47+
48+
AuthRecipeUserInfo getPrimaryUserById(AppIdentifier appIdentifier, String userId) throws StorageQueryException;
49+
50+
String getPrimaryUserIdStrForUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException;
51+
52+
AuthRecipeUserInfo[] listPrimaryUsersByEmail(TenantIdentifier tenantIdentifier, String email)
53+
throws StorageQueryException;
54+
55+
AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber)
56+
throws StorageQueryException;
57+
58+
AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(AppIdentifier appIdentifier, String thirdPartyId, String thirdPartyUserId)
59+
throws StorageQueryException;
60+
61+
AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(TenantIdentifier tenantIdentifier, String thirdPartyId,
62+
String thirdPartyUserId) throws StorageQueryException;
63+
64+
boolean checkIfUsesAccountLinking(AppIdentifier appIdentifier) throws StorageQueryException;
65+
66+
int getUsersCountWithMoreThanOneLoginMethod(AppIdentifier appIdentifier) throws StorageQueryException;
4767
}

src/main/java/io/supertokens/pluginInterface/authRecipe/AuthRecipeUserInfo.java

Lines changed: 217 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,232 @@
1616

1717
package io.supertokens.pluginInterface.authRecipe;
1818

19+
import com.google.gson.JsonArray;
20+
import com.google.gson.JsonObject;
21+
import com.google.gson.JsonPrimitive;
1922
import io.supertokens.pluginInterface.RECIPE_ID;
2023

21-
public abstract class AuthRecipeUserInfo {
24+
import java.util.*;
2225

23-
public String id;
26+
public class AuthRecipeUserInfo {
27+
28+
private final String id;
29+
30+
private String externalUserId = null;
31+
32+
public boolean isPrimaryUser;
33+
34+
public LoginMethod[] loginMethods;
35+
36+
public Set<String> tenantIds;
2437

2538
public long timeJoined;
2639

27-
public final String[] tenantIds;
40+
private boolean didCallSetExternalUserId = false;
41+
42+
public void setExternalUserId(String externalUserId) {
43+
didCallSetExternalUserId = true;
44+
this.externalUserId = externalUserId;
45+
for (LoginMethod loginMethod : this.loginMethods) {
46+
if (loginMethod.getSupertokensUserId().equals(this.id)) {
47+
loginMethod.setExternalUserId(externalUserId);
48+
}
49+
}
50+
}
2851

29-
public AuthRecipeUserInfo(String id, long timeJoined, String[] tenantIds) {
52+
public String getSupertokensOrExternalUserId() {
53+
assert (this.didCallSetExternalUserId);
54+
55+
if (this.externalUserId != null) {
56+
return this.externalUserId;
57+
}
58+
return this.id;
59+
}
60+
61+
public String getSupertokensUserId() {
62+
return this.id;
63+
}
64+
65+
protected AuthRecipeUserInfo(String id, Boolean isPrimaryUser, LoginMethod loginMethods) {
66+
assert (isPrimaryUser != null);
3067
this.id = id;
31-
this.timeJoined = timeJoined;
32-
this.tenantIds = tenantIds;
68+
this.isPrimaryUser = isPrimaryUser;
69+
this.loginMethods = new LoginMethod[]{loginMethods};
70+
this.timeJoined = loginMethods.timeJoined;
71+
this.tenantIds = new HashSet<>();
72+
this.tenantIds.addAll(loginMethods.tenantIds);
73+
}
74+
75+
public static AuthRecipeUserInfo create(String id, Boolean isPrimaryUser, LoginMethod loginMethod) {
76+
assert (isPrimaryUser != null);
77+
return new AuthRecipeUserInfo(id, isPrimaryUser, loginMethod);
78+
}
79+
80+
public void addLoginMethod(LoginMethod loginMethod) {
81+
for (LoginMethod method : this.loginMethods) {
82+
if (method.equals(loginMethod)) {
83+
return;
84+
}
85+
}
86+
LoginMethod[] newLoginMethods = new LoginMethod[this.loginMethods.length + 1];
87+
System.arraycopy(this.loginMethods, 0, newLoginMethods, 0, this.loginMethods.length);
88+
newLoginMethods[this.loginMethods.length] = loginMethod;
89+
this.loginMethods = Arrays.stream(newLoginMethods).sorted((o1, o2) -> {
90+
if (o1.timeJoined < o2.timeJoined) {
91+
return -1;
92+
} else if (o1.timeJoined > o2.timeJoined) {
93+
return 1;
94+
}
95+
return 0;
96+
}).toArray(LoginMethod[]::new);
97+
if (timeJoined > loginMethod.timeJoined) {
98+
this.timeJoined = loginMethod.timeJoined;
99+
}
100+
101+
this.tenantIds.addAll(loginMethod.tenantIds);
33102
}
34103

35-
public abstract RECIPE_ID getRecipeId();
104+
@Override
105+
public boolean equals(Object other) {
106+
if (!(other instanceof AuthRecipeUserInfo)) {
107+
return false;
108+
}
109+
AuthRecipeUserInfo otherUser = (AuthRecipeUserInfo) other;
110+
return this.id.equals(otherUser.id) && this.isPrimaryUser == otherUser.isPrimaryUser
111+
&& this.timeJoined == otherUser.timeJoined && Arrays.equals(this.loginMethods, otherUser.loginMethods)
112+
&& this.tenantIds.equals(otherUser.tenantIds);
113+
}
114+
115+
@Override
116+
public int hashCode() {
117+
// combine hash codes of all fields
118+
// We multiply with 31 because it's a prime number.
119+
int hashCode = this.id.hashCode();
120+
hashCode = 31 * hashCode + Boolean.hashCode(this.isPrimaryUser);
121+
hashCode = 31 * hashCode + Long.hashCode(this.timeJoined);
122+
hashCode = 31 * hashCode + Arrays.hashCode(this.loginMethods);
123+
hashCode = 31 * hashCode + this.tenantIds.hashCode();
124+
return hashCode;
125+
}
36126

127+
public JsonObject toJson() {
128+
if (!didCallSetExternalUserId) {
129+
throw new RuntimeException("Found a bug: Did you forget to call setExternalUserId?");
130+
}
131+
JsonObject jsonObject = new JsonObject();
132+
jsonObject.addProperty("id", getSupertokensOrExternalUserId());
133+
jsonObject.addProperty("isPrimaryUser", this.isPrimaryUser);
134+
JsonArray tenantIds = new JsonArray();
135+
for (String tenant : this.tenantIds) {
136+
tenantIds.add(new JsonPrimitive(tenant));
137+
}
138+
jsonObject.add("tenantIds", tenantIds);
139+
jsonObject.addProperty("timeJoined", this.timeJoined);
140+
141+
// now we add unique emails, phone numbers and third party across all login methods
142+
Set<String> emails = new HashSet<>();
143+
Set<String> phoneNumbers = new HashSet<>();
144+
Set<LoginMethod.ThirdParty> thirdParty = new HashSet<>();
145+
for (LoginMethod loginMethod : this.loginMethods) {
146+
if (loginMethod.email != null) {
147+
emails.add(loginMethod.email);
148+
}
149+
if (loginMethod.phoneNumber != null) {
150+
phoneNumbers.add(loginMethod.phoneNumber);
151+
}
152+
if (loginMethod.thirdParty != null) {
153+
thirdParty.add(loginMethod.thirdParty);
154+
}
155+
}
156+
JsonArray emailsJson = new JsonArray();
157+
for (String email : emails) {
158+
emailsJson.add(new JsonPrimitive(email));
159+
}
160+
jsonObject.add("emails", emailsJson);
161+
JsonArray phoneNumbersJson = new JsonArray();
162+
for (String phoneNumber : phoneNumbers) {
163+
phoneNumbersJson.add(new JsonPrimitive(phoneNumber));
164+
}
165+
jsonObject.add("phoneNumbers", phoneNumbersJson);
166+
JsonArray thirdPartyJson = new JsonArray();
167+
for (LoginMethod.ThirdParty tpInfo : thirdParty) {
168+
JsonObject j = new JsonObject();
169+
j.addProperty("id", tpInfo.id);
170+
j.addProperty("userId", tpInfo.userId);
171+
thirdPartyJson.add(j);
172+
}
173+
jsonObject.add("thirdParty", thirdPartyJson);
174+
175+
// now we add login methods..
176+
JsonArray loginMethodsArr = new JsonArray();
177+
for (LoginMethod lM : this.loginMethods) {
178+
JsonObject lMJsonObject = new JsonObject();
179+
JsonArray lMTenantIds = new JsonArray();
180+
for (String tenant : lM.tenantIds) {
181+
lMTenantIds.add(new JsonPrimitive(tenant));
182+
}
183+
lMJsonObject.add("tenantIds", lMTenantIds);
184+
lMJsonObject.addProperty("recipeUserId", lM.getSupertokensOrExternalUserId());
185+
lMJsonObject.addProperty("verified", lM.verified);
186+
lMJsonObject.addProperty("timeJoined", lM.timeJoined);
187+
lMJsonObject.addProperty("recipeId", lM.recipeId.toString());
188+
if (lM.email != null) {
189+
lMJsonObject.addProperty("email", lM.email);
190+
}
191+
if (lM.phoneNumber != null) {
192+
lMJsonObject.addProperty("phoneNumber", lM.phoneNumber);
193+
}
194+
if (lM.thirdParty != null) {
195+
JsonObject thirdPartyJsonObject = new JsonObject();
196+
thirdPartyJsonObject.addProperty("id", lM.thirdParty.id);
197+
thirdPartyJsonObject.addProperty("userId", lM.thirdParty.userId);
198+
lMJsonObject.add("thirdParty", thirdPartyJsonObject);
199+
}
200+
loginMethodsArr.add(lMJsonObject);
201+
}
202+
jsonObject.add("loginMethods", loginMethodsArr);
203+
return jsonObject;
204+
}
205+
206+
public JsonObject toJsonWithoutAccountLinking() {
207+
if (!didCallSetExternalUserId) {
208+
throw new RuntimeException("Found a bug: Did you forget to call setExternalUserId?");
209+
}
210+
// this is for older CDI versions.
211+
if (this.loginMethods.length != 1) {
212+
throw new IllegalStateException(
213+
"Please use a CDI version that is greater than the one in which account linking feature was " +
214+
"enabled.");
215+
}
216+
LoginMethod loginMethod = loginMethods[0];
217+
JsonObject jsonObject = new JsonObject();
218+
jsonObject.addProperty("id", loginMethod.getSupertokensOrExternalUserId());
219+
jsonObject.addProperty("timeJoined", loginMethod.timeJoined);
220+
JsonArray tenantIds = new JsonArray();
221+
for (String tenant : loginMethod.tenantIds) {
222+
tenantIds.add(new JsonPrimitive(tenant));
223+
}
224+
jsonObject.add("tenantIds", tenantIds);
225+
if (loginMethod.recipeId == RECIPE_ID.EMAIL_PASSWORD) {
226+
jsonObject.addProperty("email", loginMethod.email);
227+
} else if (loginMethod.recipeId == RECIPE_ID.THIRD_PARTY) {
228+
jsonObject.addProperty("email", loginMethod.email);
229+
JsonObject thirdPartyJson = new JsonObject();
230+
assert loginMethod.thirdParty != null;
231+
thirdPartyJson.addProperty("id", loginMethod.thirdParty.id);
232+
thirdPartyJson.addProperty("userId", loginMethod.thirdParty.userId);
233+
jsonObject.add("thirdParty", thirdPartyJson);
234+
} else if (loginMethod.recipeId == RECIPE_ID.PASSWORDLESS) {
235+
if (loginMethod.email != null) {
236+
jsonObject.addProperty("email", loginMethod.email);
237+
}
238+
if (loginMethod.phoneNumber != null) {
239+
jsonObject.addProperty("phoneNumber", loginMethod.phoneNumber);
240+
}
241+
} else {
242+
throw new UnsupportedOperationException("Please search for bugs");
243+
}
244+
245+
return jsonObject;
246+
}
37247
}

0 commit comments

Comments
 (0)