diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index bf6de6dfb..94c0f5211 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -34,7 +34,7 @@ highlighting the necessary changes)
- To know which one it is, run find the latest released tag (`git tag`) in the format `vX.Y.Z`, and then find the
latest branch (`git branch --all`) whose `X.Y` is greater than the latest released tag.
- If no such branch exists, then create one from the latest released branch.
-
+- [ ] If added a foreign key constraint on `app_id_to_user_id` table, make sure to delete from this table when deleting the user as well if `deleteUserIdMappingToo` is false.
## Remaining TODOs for this PR
- [ ] Item1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab165dc52..562876542 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,187 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [unreleased]
+## [7.0.0] - 2023-09-19
+
+- Support for CDI version 4.0
+- Adds Account Linking feature
+
+### Session recipe changes
+
+- New access token version: v5, which contains a required prop: `rsub`. This contains the recipe user ID that belongs to the login method that the user used to login. The `sub` claim in the access token payload is now the primary user ID.
+- APIs that return `SessionInformation` (like GET `/recipe/session`) contains userId, recipeUserId in the response.
+- Apis that create / modify / refresh a session return the `recipeUserId` in the `session` object in the response.
+- Token theft detected response returns userId and recipeUserId
+
+### Db Schema changes
+
+- Adds columns `primary_or_recipe_user_id`, `is_linked_or_is_a_primary_user` and `primary_or_recipe_user_time_joined` to `all_auth_recipe_users` table
+- Adds columns `primary_or_recipe_user_id` and `is_linked_or_is_a_primary_user` to `app_id_to_user_id` table
+- Removes index `all_auth_recipe_users_pagination_index` and addes `all_auth_recipe_users_pagination_index1`,
+ `all_auth_recipe_users_pagination_index2`, `all_auth_recipe_users_pagination_index3` and
+ `all_auth_recipe_users_pagination_index4` indexes instead on `all_auth_recipe_users` table
+- Adds `all_auth_recipe_users_recipe_id_index` on `all_auth_recipe_users` table
+- Adds `all_auth_recipe_users_primary_user_id_index` on `all_auth_recipe_users` table
+- Adds `email` column to `emailpassword_pswd_reset_tokens` table
+- Changes `user_id` foreign key constraint on `emailpassword_pswd_reset_tokens` to `app_id_to_user_id` table
+
+### Migration steps for SQL
+
+1. Ensure that the core is already upgraded to version 6.0.13 (CDI version 3.0)
+2. Stop the core instance(s)
+3. Run the migration script
+
+
+
+ If using PostgreSQL
+
+ ```sql
+ ALTER TABLE all_auth_recipe_users
+ ADD COLUMN primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0');
+
+ ALTER TABLE all_auth_recipe_users
+ ADD COLUMN is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE;
+
+ ALTER TABLE all_auth_recipe_users
+ ADD COLUMN primary_or_recipe_user_time_joined BIGINT NOT NULL DEFAULT 0;
+
+ UPDATE all_auth_recipe_users
+ SET primary_or_recipe_user_id = user_id
+ WHERE primary_or_recipe_user_id = '0';
+
+ UPDATE all_auth_recipe_users
+ SET primary_or_recipe_user_time_joined = time_joined
+ WHERE primary_or_recipe_user_time_joined = 0;
+
+ ALTER TABLE all_auth_recipe_users
+ ADD CONSTRAINT all_auth_recipe_users_primary_or_recipe_user_id_fkey
+ FOREIGN KEY (app_id, primary_or_recipe_user_id)
+ REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE;
+
+ ALTER TABLE all_auth_recipe_users
+ ALTER primary_or_recipe_user_id DROP DEFAULT;
+
+ ALTER TABLE app_id_to_user_id
+ ADD COLUMN primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0');
+
+ ALTER TABLE app_id_to_user_id
+ ADD COLUMN is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE;
+
+ UPDATE app_id_to_user_id
+ SET primary_or_recipe_user_id = user_id
+ WHERE primary_or_recipe_user_id = '0';
+
+ ALTER TABLE app_id_to_user_id
+ ADD CONSTRAINT app_id_to_user_id_primary_or_recipe_user_id_fkey
+ FOREIGN KEY (app_id, primary_or_recipe_user_id)
+ REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE;
+
+ ALTER TABLE app_id_to_user_id
+ ALTER primary_or_recipe_user_id DROP DEFAULT;
+
+ DROP INDEX all_auth_recipe_users_pagination_index;
+
+ CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users (
+ app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users (
+ app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users (
+ recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users (
+ recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users (primary_or_recipe_user_id, app_id);
+
+ CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users (app_id, recipe_id, tenant_id);
+
+ ALTER TABLE emailpassword_pswd_reset_tokens DROP CONSTRAINT IF EXISTS emailpassword_pswd_reset_tokens_user_id_fkey;
+
+ ALTER TABLE emailpassword_pswd_reset_tokens ADD CONSTRAINT emailpassword_pswd_reset_tokens_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE;
+
+ ALTER TABLE emailpassword_pswd_reset_tokens ADD COLUMN email VARCHAR(256);
+ ```
+
+
+
+
+ If using MySQL
+
+ ```sql
+ ALTER TABLE all_auth_recipe_users
+ ADD primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0');
+
+ ALTER TABLE all_auth_recipe_users
+ ADD is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE;
+
+ ALTER TABLE all_auth_recipe_users
+ ADD primary_or_recipe_user_time_joined BIGINT UNSIGNED NOT NULL DEFAULT 0;
+
+ UPDATE all_auth_recipe_users
+ SET primary_or_recipe_user_id = user_id
+ WHERE primary_or_recipe_user_id = '0';
+
+ UPDATE all_auth_recipe_users
+ SET primary_or_recipe_user_time_joined = time_joined
+ WHERE primary_or_recipe_user_time_joined = 0;
+
+ ALTER TABLE all_auth_recipe_users
+ ADD FOREIGN KEY (app_id, primary_or_recipe_user_id)
+ REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE;
+
+ ALTER TABLE all_auth_recipe_users
+ ALTER primary_or_recipe_user_id DROP DEFAULT;
+
+ ALTER TABLE app_id_to_user_id
+ ADD primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0');
+
+ ALTER TABLE app_id_to_user_id
+ ADD is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE;
+
+ UPDATE app_id_to_user_id
+ SET primary_or_recipe_user_id = user_id
+ WHERE primary_or_recipe_user_id = '0';
+
+ ALTER TABLE app_id_to_user_id
+ ADD FOREIGN KEY (app_id, primary_or_recipe_user_id)
+ REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE;
+
+ ALTER TABLE app_id_to_user_id
+ ALTER primary_or_recipe_user_id DROP DEFAULT;
+
+ DROP INDEX all_auth_recipe_users_pagination_index ON all_auth_recipe_users;
+
+ CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users (
+ app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users (
+ app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users (
+ recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users (
+ recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);
+
+ CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users (primary_or_recipe_user_id, app_id);
+
+ CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users (app_id, recipe_id, tenant_id);
+
+ ALTER TABLE emailpassword_pswd_reset_tokens
+ DROP FOREIGN KEY emailpassword_pswd_reset_tokens_ibfk_1;
+
+ ALTER TABLE emailpassword_pswd_reset_tokens
+ ADD FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE;
+
+ ALTER TABLE emailpassword_pswd_reset_tokens ADD email VARCHAR(256);
+ ```
+
+
+
+4. Start the new instance(s) of the core (version 7.0.0)
+
## [6.0.13] - 2023-09-15
- Fixes paid stats reporting for multitenancy
@@ -86,8 +267,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Support for multitenancy.
- New config `supertokens_saas_secret` added to support multitenancy in SaaS mode.
-- New config `supertokens_default_cdi_version` is added to specify the version of CDI core must assume when the
- version is not specified in the request. If this config is not specified, the core will assume the latest version.
+- New config `supertokens_default_cdi_version` is added to specify the version of CDI core must assume when the version
+ is not specified in the request. If this config is not specified, the core will assume the latest version.
### Fixes
@@ -1811,7 +1992,6 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
"key_string": "$keys.value",
"algorithm": "RS256",
"created_at": "$keys.created_at_time",
-
}
},
{
@@ -1826,7 +2006,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
}
}
- ]);
+ ]);
```
- If using `access_token_signing_key_dynamic` true or not set:
diff --git a/build.gradle b/build.gradle
index 5ae32374e..a2c2d397a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}
-version = "6.0.13"
+version = "7.0.0"
repositories {
diff --git a/cli/jar/cli.jar b/cli/jar/cli.jar
index a5677b8fa..e34e8ae49 100644
Binary files a/cli/jar/cli.jar and b/cli/jar/cli.jar differ
diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json
index 1b1bc74f9..0c9d09fe0 100644
--- a/coreDriverInterfaceSupported.json
+++ b/coreDriverInterfaceSupported.json
@@ -16,6 +16,7 @@
"2.19",
"2.20",
"2.21",
- "3.0"
+ "3.0",
+ "4.0"
]
}
\ No newline at end of file
diff --git a/downloader/jar/downloader.jar b/downloader/jar/downloader.jar
index 72dfc6701..7a4eb7a87 100644
Binary files a/downloader/jar/downloader.jar and b/downloader/jar/downloader.jar differ
diff --git a/ee/jar/ee.jar b/ee/jar/ee.jar
index 38b5ff04a..11e6be2a9 100644
Binary files a/ee/jar/ee.jar and b/ee/jar/ee.jar differ
diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java
index 56f02d526..b68622959 100644
--- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java
+++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java
@@ -272,6 +272,49 @@ private JsonObject getMultiTenancyStats()
return stats;
}
+ private JsonObject getAccountLinkingStats() throws StorageQueryException {
+ JsonObject result = new JsonObject();
+ Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier);
+ boolean usesAccountLinking = false;
+
+ for (Storage storage : storages) {
+ if (((AuthRecipeStorage)storage).checkIfUsesAccountLinking(this.appIdentifier)) {
+ usesAccountLinking = true;
+ break;
+ }
+ }
+
+ result.addProperty("usesAccountLinking", usesAccountLinking);
+ if (!usesAccountLinking) {
+ result.addProperty("totalUserCountWithMoreThanOneLoginMethod", 0);
+ JsonArray mauArray = new JsonArray();
+ for (int i = 0; i < 30; i++) {
+ mauArray.add(new JsonPrimitive(0));
+ }
+ result.add("mauWithMoreThanOneLoginMethod", mauArray);
+ return result;
+ }
+
+ int totalUserCountWithMoreThanOneLoginMethod = 0;
+ int[] maus = new int[30];
+
+ long now = System.currentTimeMillis();
+ long today = now - (now % (24 * 60 * 60 * 1000L));
+
+ for (Storage storage : storages) {
+ totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage)storage).getUsersCountWithMoreThanOneLoginMethod(this.appIdentifier);
+
+ for (int i = 0; i < 30; i++) {
+ long timestamp = today - (i * 24 * 60 * 60 * 1000L);
+ maus[i] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(appIdentifier, timestamp);
+ }
+ }
+
+ result.addProperty("totalUserCountWithMoreThanOneLoginMethod", totalUserCountWithMoreThanOneLoginMethod);
+ result.add("mauWithMoreThanOneLoginMethod", new Gson().toJsonTree(maus));
+ return result;
+ }
+
private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException {
JsonArray mauArr = new JsonArray();
for (int i = 0; i < 30; i++) {
@@ -319,6 +362,10 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp
if (feature == EE_FEATURES.MULTI_TENANCY) {
usageStats.add(EE_FEATURES.MULTI_TENANCY.toString(), getMultiTenancyStats());
}
+
+ if (feature == EE_FEATURES.ACCOUNT_LINKING) {
+ usageStats.add(EE_FEATURES.ACCOUNT_LINKING.toString(), getAccountLinkingStats());
+ }
}
usageStats.add("maus", getMAUs());
diff --git a/jar/core-6.0.13.jar b/jar/core-7.0.0.jar
similarity index 52%
rename from jar/core-6.0.13.jar
rename to jar/core-7.0.0.jar
index d7483dd77..723342c67 100644
Binary files a/jar/core-6.0.13.jar and b/jar/core-7.0.0.jar differ
diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json
index 431c3b083..a5fdc62cd 100644
--- a/pluginInterfaceSupported.json
+++ b/pluginInterfaceSupported.json
@@ -1,6 +1,6 @@
{
"_comment": "contains a list of plugin interfaces branch names that this core supports",
"versions": [
- "3.0"
+ "4.0"
]
}
\ No newline at end of file
diff --git a/src/main/java/io/supertokens/ActiveUsers.java b/src/main/java/io/supertokens/ActiveUsers.java
index 55224cb49..7c4601958 100644
--- a/src/main/java/io/supertokens/ActiveUsers.java
+++ b/src/main/java/io/supertokens/ActiveUsers.java
@@ -1,6 +1,8 @@
package io.supertokens;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
@@ -37,4 +39,18 @@ public static int countUsersActiveSince(Main main, long time)
return countUsersActiveSince(new AppIdentifierWithStorage(null, null, StorageLayer.getStorage(main)), main,
time);
}
+
+ public static void removeActiveUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
+ throws StorageQueryException {
+ try {
+ ((AuthRecipeSQLStorage) appIdentifierWithStorage.getActiveUsersStorage()).startTransaction(con -> {
+ appIdentifierWithStorage.getActiveUsersStorage().deleteUserActive_Transaction(con, appIdentifierWithStorage, userId);
+ ((AuthRecipeSQLStorage) appIdentifierWithStorage.getActiveUsersStorage()).commitTransaction(con);
+ return null;
+ });
+
+ } catch (StorageTransactionLogicException e) {
+ throw new StorageQueryException(e.actualException);
+ }
+ }
}
diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java
index d14c38e6b..8596a28d8 100644
--- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java
+++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java
@@ -17,26 +17,39 @@
package io.supertokens.authRecipe;
import io.supertokens.Main;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlag;
+import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
-import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
-import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage;
+import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
+import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
+import io.supertokens.session.Session;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.useridmapping.UserIdType;
import org.jetbrains.annotations.TestOnly;
import javax.annotation.Nullable;
+import java.util.*;
/*This files contains functions that are common for all auth recipes*/
@@ -44,6 +57,586 @@ public class AuthRecipe {
public static final int USER_PAGINATION_LIMIT = 500;
+ @TestOnly
+ public static boolean unlinkAccounts(Main main, String recipeUserId)
+ throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException {
+ AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null,
+ StorageLayer.getStorage(main));
+ return unlinkAccounts(main, appId, recipeUserId);
+ }
+
+
+ // returns true if the input user ID was deleted - which can happens if it was a primary user id and
+ // there were other accounts linked to it as well.
+ public static boolean unlinkAccounts(Main main, AppIdentifierWithStorage appIdentifierWithStorage,
+ String recipeUserId)
+ throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException {
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+ try {
+ UnlinkResult res = storage.startTransaction(con -> {
+ AuthRecipeUserInfo primaryUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con,
+ recipeUserId);
+ if (primaryUser == null) {
+ throw new StorageTransactionLogicException(new UnknownUserIdException());
+ }
+
+ if (!primaryUser.isPrimaryUser) {
+ throw new StorageTransactionLogicException(new InputUserIdIsNotAPrimaryUserException(recipeUserId));
+ }
+
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult =
+ io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
+ appIdentifierWithStorage,
+ recipeUserId, UserIdType.SUPERTOKENS);
+
+ if (primaryUser.getSupertokensUserId().equals(recipeUserId)) {
+ // we are trying to unlink the user ID which is the same as the primary one.
+ if (primaryUser.loginMethods.length == 1) {
+ storage.unlinkAccounts_Transaction(appIdentifierWithStorage, con, primaryUser.getSupertokensUserId(), recipeUserId);
+ return new UnlinkResult(mappingResult == null ? recipeUserId : mappingResult.externalUserId, false);
+ } else {
+ // Here we delete the recipe user id cause if we just unlink, then there will be two
+ // distinct users with the same ID - which is a broken state.
+ // The delete will also cause the automatic unlinking.
+ // We need to make sure that it only deletes sessions for recipeUserId and not other linked
+ // users who have their sessions for primaryUserId (that is equal to the recipeUserId)
+ deleteUserHelper(con, appIdentifierWithStorage, recipeUserId, false, mappingResult);
+ return new UnlinkResult(mappingResult == null ? recipeUserId : mappingResult.externalUserId, true);
+ }
+ } else {
+ storage.unlinkAccounts_Transaction(appIdentifierWithStorage, con, primaryUser.getSupertokensUserId(), recipeUserId);
+ return new UnlinkResult(mappingResult == null ? recipeUserId : mappingResult.externalUserId, false);
+ }
+ });
+ Session.revokeAllSessionsForUser(main, appIdentifierWithStorage, res.userId, false);
+ return res.wasLinked;
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof UnknownUserIdException) {
+ throw (UnknownUserIdException) e.actualException;
+ } else if (e.actualException instanceof InputUserIdIsNotAPrimaryUserException) {
+ throw (InputUserIdIsNotAPrimaryUserException) e.actualException;
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
+ @TestOnly
+ public static AuthRecipeUserInfo getUserById(Main main, String userId)
+ throws StorageQueryException {
+ AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null,
+ StorageLayer.getStorage(main));
+ return getUserById(appId, userId);
+ }
+
+ public static AuthRecipeUserInfo getUserById(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
+ throws StorageQueryException {
+ return appIdentifierWithStorage.getAuthRecipeStorage().getPrimaryUserById(appIdentifierWithStorage, userId);
+ }
+
+ public static class CreatePrimaryUserResult {
+ public AuthRecipeUserInfo user;
+ public boolean wasAlreadyAPrimaryUser;
+
+ public CreatePrimaryUserResult(AuthRecipeUserInfo user, boolean wasAlreadyAPrimaryUser) {
+ this.user = user;
+ this.wasAlreadyAPrimaryUser = wasAlreadyAPrimaryUser;
+ }
+ }
+
+ public static class CanLinkAccountsResult {
+ public String recipeUserId;
+ public String primaryUserId;
+
+ public boolean alreadyLinked;
+
+ public CanLinkAccountsResult(String recipeUserId, String primaryUserId, boolean alreadyLinked) {
+ this.recipeUserId = recipeUserId;
+ this.primaryUserId = primaryUserId;
+ this.alreadyLinked = alreadyLinked;
+ }
+ }
+
+ @TestOnly
+ public static CanLinkAccountsResult canLinkAccounts(Main main, String recipeUserId, String primaryUserId)
+ throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException,
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException,
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
+ AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null,
+ StorageLayer.getStorage(main));
+ return canLinkAccounts(appId, recipeUserId, primaryUserId);
+ }
+
+ public static CanLinkAccountsResult canLinkAccounts(AppIdentifierWithStorage appIdentifierWithStorage,
+ String recipeUserId, String primaryUserId)
+ throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException,
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException,
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+ try {
+ return storage.startTransaction(con -> {
+ try {
+ CanLinkAccountsResult result = canLinkAccountsHelper(con, appIdentifierWithStorage,
+ recipeUserId, primaryUserId);
+
+ storage.commitTransaction(con);
+
+ return result;
+ } catch (UnknownUserIdException | InputUserIdIsNotAPrimaryUserException |
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException |
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof UnknownUserIdException) {
+ throw (UnknownUserIdException) e.actualException;
+ } else if (e.actualException instanceof InputUserIdIsNotAPrimaryUserException) {
+ throw (InputUserIdIsNotAPrimaryUserException) e.actualException;
+ } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) {
+ throw (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) e.actualException;
+ } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) {
+ throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
+
+ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection con,
+ AppIdentifierWithStorage appIdentifierWithStorage,
+ String _recipeUserId, String _primaryUserId)
+ throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException,
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException,
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+ AuthRecipeUserInfo primaryUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con,
+ _primaryUserId);
+
+ if (primaryUser == null) {
+ throw new UnknownUserIdException();
+ }
+
+ if (!primaryUser.isPrimaryUser) {
+ throw new InputUserIdIsNotAPrimaryUserException(primaryUser.getSupertokensUserId());
+ }
+
+ AuthRecipeUserInfo recipeUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con,
+ _recipeUserId);
+ if (recipeUser == null) {
+ throw new UnknownUserIdException();
+ }
+
+ if (recipeUser.isPrimaryUser) {
+ if (recipeUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
+ return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), true);
+ } else {
+ throw new RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(recipeUser,
+ "The input recipe user ID is already linked to another user ID");
+ }
+ }
+
+ // now we know that the recipe user ID is not a primary user, so we can focus on it's one
+ // login method
+ assert (recipeUser.loginMethods.length == 1);
+ LoginMethod recipeUserIdLM = recipeUser.loginMethods[0];
+
+ Set tenantIds = new HashSet<>();
+ tenantIds.addAll(recipeUser.tenantIds);
+ tenantIds.addAll(primaryUser.tenantIds);
+
+ // we loop through the union of both the user's tenantIds and check that the criteria for
+ // linking accounts is not violated in any of them. We do a union and not an intersection
+ // cause if we did an intersection, and that yields that account linking is allowed, it could
+ // result in one tenant having two primary users with the same email. For example:
+ // - tenant1 has u1 with email e, and u2 with email e, primary user (one is ep, one is tp)
+ // - tenant2 has u3 with email e, primary user (passwordless)
+ // now if we want to link u3 with u1, we have to deny it cause if we don't, it will result in
+ // u1 and u2 to be primary users with the same email in the same tenant. If we do an
+ // intersection, we will get an empty set, but if we do a union, we will get both the tenants and
+ // do the checks in both.
+ for (String tenantId : tenantIds) {
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(
+ appIdentifierWithStorage.getConnectionUriDomain(), appIdentifierWithStorage.getAppId(),
+ tenantId);
+ // we do not bother with getting the tenantIdentifierWithStorage here because
+ // we get the tenants from the user itself, and the user can only be shared across
+ // tenants of the same storage - therefore, the storage will be the same.
+
+ if (recipeUserIdLM.email != null) {
+ AuthRecipeUserInfo[] usersWithSameEmail = storage
+ .listPrimaryUsersByEmail_Transaction(appIdentifierWithStorage, con,
+ recipeUserIdLM.email);
+ for (AuthRecipeUserInfo user : usersWithSameEmail) {
+ if (!user.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
+ throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(),
+ "This user's email is already associated with another user ID");
+ }
+ }
+ }
+
+ if (recipeUserIdLM.phoneNumber != null) {
+ AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage
+ .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con,
+ recipeUserIdLM.phoneNumber);
+ for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
+ if (!user.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
+ throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(),
+ "This user's phone number is already associated with another user" +
+ " ID");
+ }
+ }
+ }
+
+ if (recipeUserIdLM.thirdParty != null) {
+ AuthRecipeUserInfo[] usersWithSameThirdParty = storage
+ .listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifierWithStorage, con,
+ recipeUserIdLM.thirdParty.id, recipeUserIdLM.thirdParty.userId);
+ for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) {
+ if (!userWithSameThirdParty.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (userWithSameThirdParty.isPrimaryUser &&
+ !userWithSameThirdParty.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
+ throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
+ userWithSameThirdParty.getSupertokensUserId(),
+ "This user's third party login is already associated with another" +
+ " user ID");
+ }
+ }
+
+ }
+ }
+
+ return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), false);
+ }
+
+ @TestOnly
+ public static LinkAccountsResult linkAccounts(Main main, String recipeUserId, String primaryUserId)
+ throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
+ UnknownUserIdException,
+ FeatureNotEnabledException, InputUserIdIsNotAPrimaryUserException,
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException {
+ AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null,
+ StorageLayer.getStorage(main));
+ try {
+ return linkAccounts(main, appId, recipeUserId, primaryUserId);
+ } catch (TenantOrAppNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static LinkAccountsResult linkAccounts(Main main, AppIdentifierWithStorage appIdentifierWithStorage,
+ String _recipeUserId, String _primaryUserId)
+ throws StorageQueryException,
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, InputUserIdIsNotAPrimaryUserException,
+ UnknownUserIdException, TenantOrAppNotFoundException, FeatureNotEnabledException {
+
+ if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifierWithStorage).getEnabledFeatures())
+ .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING)) {
+ throw new FeatureNotEnabledException(
+ "Account linking feature is not enabled for this app. Please contact support to enable it.");
+ }
+
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+ try {
+ LinkAccountsResult result = storage.startTransaction(con -> {
+
+ try {
+ CanLinkAccountsResult canLinkAccounts = canLinkAccountsHelper(con, appIdentifierWithStorage,
+ _recipeUserId, _primaryUserId);
+
+ if (canLinkAccounts.alreadyLinked) {
+ return new LinkAccountsResult(getUserById(appIdentifierWithStorage, canLinkAccounts.primaryUserId), true);
+ }
+ // now we can link accounts in the db.
+ storage.linkAccounts_Transaction(appIdentifierWithStorage, con, canLinkAccounts.recipeUserId,
+ canLinkAccounts.primaryUserId);
+
+ storage.commitTransaction(con);
+
+ return new LinkAccountsResult(getUserById(appIdentifierWithStorage, canLinkAccounts.primaryUserId), false);
+ } catch (UnknownUserIdException | InputUserIdIsNotAPrimaryUserException |
+ RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException |
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+
+ if (!result.wasAlreadyLinked) {
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult =
+ io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
+ appIdentifierWithStorage,
+ _recipeUserId, UserIdType.SUPERTOKENS);
+ // finally, we revoke all sessions of the recipeUser Id cause their user ID has changed.
+ Session.revokeAllSessionsForUser(main, appIdentifierWithStorage,
+ mappingResult == null ? _recipeUserId : mappingResult.externalUserId, false);
+ }
+
+ return result;
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof UnknownUserIdException) {
+ throw (UnknownUserIdException) e.actualException;
+ } else if (e.actualException instanceof InputUserIdIsNotAPrimaryUserException) {
+ throw (InputUserIdIsNotAPrimaryUserException) e.actualException;
+ } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) {
+ throw (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) e.actualException;
+ } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) {
+ throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
+
+ public static class LinkAccountsResult {
+ public final AuthRecipeUserInfo user;
+ public final boolean wasAlreadyLinked;
+
+ public LinkAccountsResult(AuthRecipeUserInfo user, boolean wasAlreadyLinked) {
+ this.user = user;
+ this.wasAlreadyLinked = wasAlreadyLinked;
+ }
+ }
+
+ @TestOnly
+ public static CreatePrimaryUserResult canCreatePrimaryUser(Main main,
+ String recipeUserId)
+ throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
+ RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException {
+ AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null,
+ StorageLayer.getStorage(main));
+ return canCreatePrimaryUser(appId, recipeUserId);
+ }
+
+ public static CreatePrimaryUserResult canCreatePrimaryUser(AppIdentifierWithStorage appIdentifierWithStorage,
+ String recipeUserId)
+ throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
+ RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException {
+
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+ try {
+ return storage.startTransaction(con -> {
+ try {
+ return canCreatePrimaryUserHelper(con, appIdentifierWithStorage,
+ recipeUserId);
+
+ } catch (UnknownUserIdException | RecipeUserIdAlreadyLinkedWithPrimaryUserIdException |
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof UnknownUserIdException) {
+ throw (UnknownUserIdException) e.actualException;
+ } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) {
+ throw (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) e.actualException;
+ } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) {
+ throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
+
+ private static CreatePrimaryUserResult canCreatePrimaryUserHelper(TransactionConnection con,
+ AppIdentifierWithStorage appIdentifierWithStorage,
+ String recipeUserId)
+ throws StorageQueryException, UnknownUserIdException, RecipeUserIdAlreadyLinkedWithPrimaryUserIdException,
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+
+ AuthRecipeUserInfo targetUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con,
+ recipeUserId);
+ if (targetUser == null) {
+ throw new UnknownUserIdException();
+ }
+ if (targetUser.isPrimaryUser) {
+ if (targetUser.getSupertokensUserId().equals(recipeUserId)) {
+ return new CreatePrimaryUserResult(targetUser, true);
+ } else {
+ throw new RecipeUserIdAlreadyLinkedWithPrimaryUserIdException(targetUser.getSupertokensUserId(),
+ "This user ID is already linked to another user ID");
+ }
+ }
+
+ // this means that the user has only one login method since it's not a primary user
+ // nor is it linked to a primary user
+ assert (targetUser.loginMethods.length == 1);
+ LoginMethod loginMethod = targetUser.loginMethods[0];
+
+ for (String tenantId : targetUser.tenantIds) {
+ if (loginMethod.email != null) {
+ AuthRecipeUserInfo[] usersWithSameEmail = storage
+ .listPrimaryUsersByEmail_Transaction(appIdentifierWithStorage, con,
+ loginMethod.email);
+ for (AuthRecipeUserInfo user : usersWithSameEmail) {
+ if (!user.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (user.isPrimaryUser) {
+ throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(),
+ "This user's email is already associated with another user ID");
+ }
+ }
+ }
+
+ if (loginMethod.phoneNumber != null) {
+ AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage
+ .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con,
+ loginMethod.phoneNumber);
+ for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
+ if (!user.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (user.isPrimaryUser) {
+ throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(),
+ "This user's phone number is already associated with another user" +
+ " ID");
+ }
+ }
+ }
+
+ if (loginMethod.thirdParty != null) {
+ AuthRecipeUserInfo[] usersWithSameThirdParty = storage
+ .listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifierWithStorage, con,
+ loginMethod.thirdParty.id, loginMethod.thirdParty.userId);
+ for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) {
+ if (!userWithSameThirdParty.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (userWithSameThirdParty.isPrimaryUser) {
+ throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
+ userWithSameThirdParty.getSupertokensUserId(),
+ "This user's third party login is already associated with another" +
+ " user ID");
+ }
+ }
+ }
+ }
+
+ return new CreatePrimaryUserResult(targetUser, false);
+ }
+
+ @TestOnly
+ public static CreatePrimaryUserResult createPrimaryUser(Main main,
+ String recipeUserId)
+ throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
+ RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException,
+ FeatureNotEnabledException {
+ AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null,
+ StorageLayer.getStorage(main));
+ try {
+ return createPrimaryUser(main, appId, recipeUserId);
+ } catch (TenantOrAppNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static CreatePrimaryUserResult createPrimaryUser(Main main,
+ AppIdentifierWithStorage appIdentifierWithStorage,
+ String recipeUserId)
+ throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
+ RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException, TenantOrAppNotFoundException,
+ FeatureNotEnabledException {
+
+ if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifierWithStorage).getEnabledFeatures())
+ .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING)) {
+ throw new FeatureNotEnabledException(
+ "Account linking feature is not enabled for this app. Please contact support to enable it.");
+ }
+
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+ try {
+ return storage.startTransaction(con -> {
+
+ try {
+ CreatePrimaryUserResult result = canCreatePrimaryUserHelper(con, appIdentifierWithStorage,
+ recipeUserId);
+ if (result.wasAlreadyAPrimaryUser) {
+ return result;
+ }
+ storage.makePrimaryUser_Transaction(appIdentifierWithStorage, con, result.user.getSupertokensUserId());
+
+ storage.commitTransaction(con);
+
+ result.user.isPrimaryUser = true;
+
+ return result;
+ } catch (UnknownUserIdException | RecipeUserIdAlreadyLinkedWithPrimaryUserIdException |
+ AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof UnknownUserIdException) {
+ throw (UnknownUserIdException) e.actualException;
+ } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) {
+ throw (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) e.actualException;
+ } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) {
+ throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
+
+ public static AuthRecipeUserInfo[] getUsersByAccountInfo(TenantIdentifierWithStorage tenantIdentifier,
+ boolean doUnionOfAccountInfo, String email,
+ String phoneNumber, String thirdPartyId,
+ String thirdPartyUserId)
+ throws StorageQueryException {
+ Set result = new HashSet<>();
+
+ if (email != null) {
+ AuthRecipeUserInfo[] users = tenantIdentifier.getAuthRecipeStorage()
+ .listPrimaryUsersByEmail(tenantIdentifier, email);
+ result.addAll(List.of(users));
+ }
+ if (phoneNumber != null) {
+ AuthRecipeUserInfo[] users = tenantIdentifier.getAuthRecipeStorage()
+ .listPrimaryUsersByPhoneNumber(tenantIdentifier, phoneNumber);
+ result.addAll(List.of(users));
+ }
+ if (thirdPartyId != null && thirdPartyUserId != null) {
+ AuthRecipeUserInfo user = tenantIdentifier.getAuthRecipeStorage()
+ .getPrimaryUserByThirdPartyInfo(tenantIdentifier, thirdPartyId, thirdPartyUserId);
+ if (user != null) {
+ result.add(user);
+ }
+ }
+
+ if (doUnionOfAccountInfo) {
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ } else {
+ List finalList = new ArrayList<>();
+ for (AuthRecipeUserInfo user : result) {
+ boolean emailMatch = email == null;
+ boolean phoneNumberMatch = phoneNumber == null;
+ boolean thirdPartyMatch = thirdPartyId == null;
+ for (LoginMethod lM : user.loginMethods) {
+ if (email != null && email.equals(lM.email)) {
+ emailMatch = true;
+ }
+ if (phoneNumber != null && phoneNumber.equals(lM.phoneNumber)) {
+ phoneNumberMatch = true;
+ }
+ if (thirdPartyId != null &&
+ (new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId)).equals(lM.thirdParty)) {
+ thirdPartyMatch = true;
+ }
+ }
+ if (emailMatch && phoneNumberMatch && thirdPartyMatch) {
+ finalList.add(user);
+ }
+ }
+ return finalList.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ }
+
public static long getUsersCountForTenant(TenantIdentifierWithStorage tenantIdentifier,
RECIPE_ID[] includeRecipeIds)
throws StorageQueryException,
@@ -110,7 +703,7 @@ public static UserPaginationContainer getUsers(TenantIdentifierWithStorage tenan
int maxLoop = users.length;
if (users.length == limit + 1) {
maxLoop = limit;
- nextPaginationToken = new UserPaginationToken(users[limit].id,
+ nextPaginationToken = new UserPaginationToken(users[limit].getSupertokensUserId(),
users[limit].timeJoined).generateToken();
}
AuthRecipeUserInfo[] resultUsers = new AuthRecipeUserInfo[maxLoop];
@@ -135,9 +728,36 @@ public static UserPaginationContainer getUsers(Main main,
}
}
+ @TestOnly
public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId,
UserIdMapping userIdMapping)
throws StorageQueryException, StorageTransactionLogicException {
+ deleteUser(appIdentifierWithStorage, userId, true, userIdMapping);
+ }
+
+ public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId,
+ boolean removeAllLinkedAccounts,
+ UserIdMapping userIdMapping)
+ throws StorageQueryException, StorageTransactionLogicException {
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+
+ storage.startTransaction(con -> {
+ deleteUserHelper(con, appIdentifierWithStorage, userId, removeAllLinkedAccounts, userIdMapping);
+ storage.commitTransaction(con);
+ return null;
+ });
+ }
+
+ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWithStorage appIdentifierWithStorage,
+ String userId,
+ boolean removeAllLinkedAccounts,
+ UserIdMapping userIdMapping)
+ throws StorageQueryException {
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
+
+ String userIdToDeleteForNonAuthRecipeForRecipeUserId;
+ String userIdToDeleteForAuthRecipe;
+
// We clean up the user last so that if anything before that throws an error, then that will throw a
// 500 to the
// developer. In this case, they expect that the user has not been deleted (which will be true). This
@@ -163,24 +783,106 @@ public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage,
// in reference to
// https://docs.google.com/spreadsheets/d/17hYV32B0aDCeLnSxbZhfRN2Y9b0LC2xUF44vV88RNAA/edit?usp=sharing
// we want to check which state the db is in
- if (appIdentifierWithStorage.getAuthRecipeStorage()
- .doesUserIdExist(appIdentifierWithStorage, userIdMapping.externalUserId)) {
+ if (((AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage())
+ .doesUserIdExist_Transaction(con, appIdentifierWithStorage, userIdMapping.externalUserId)) {
// db is in state A4
// delete only from auth tables
- deleteAuthRecipeUser(appIdentifierWithStorage, userId);
+ userIdToDeleteForAuthRecipe = userId;
+ userIdToDeleteForNonAuthRecipeForRecipeUserId = null;
} else {
// db is in state A3
// delete user from non-auth tables with externalUserId
- deleteNonAuthRecipeUser(appIdentifierWithStorage, userIdMapping.externalUserId);
- // delete user from auth tables with superTokensUserId
- deleteAuthRecipeUser(appIdentifierWithStorage, userIdMapping.superTokensUserId);
+ userIdToDeleteForAuthRecipe = userIdMapping.superTokensUserId;
+ userIdToDeleteForNonAuthRecipeForRecipeUserId = userIdMapping.externalUserId;
}
} else {
- deleteNonAuthRecipeUser(appIdentifierWithStorage, userId);
- deleteAuthRecipeUser(appIdentifierWithStorage, userId);
+ userIdToDeleteForAuthRecipe = userId;
+ userIdToDeleteForNonAuthRecipeForRecipeUserId = userId;
+ }
+
+ assert (userIdToDeleteForAuthRecipe != null);
+
+ // this user ID represents the non auth recipe stuff to delete for the primary user id
+ String primaryUserIdToDeleteNonAuthRecipe = null;
+
+ AuthRecipeUserInfo userToDelete = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con,
+ userIdToDeleteForAuthRecipe);
+
+ if (userToDelete == null) {
+ return;
+ }
+
+ if (removeAllLinkedAccounts || userToDelete.loginMethods.length == 1) {
+ if (userToDelete.getSupertokensUserId().equals(userIdToDeleteForAuthRecipe)) {
+ primaryUserIdToDeleteNonAuthRecipe = userIdToDeleteForNonAuthRecipeForRecipeUserId;
+ if (primaryUserIdToDeleteNonAuthRecipe == null) {
+ deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(),
+ true);
+ return;
+ }
+ } else {
+ // this is always type supertokens user ID cause it's from a user from the database.
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult =
+ io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
+ con,
+ appIdentifierWithStorage,
+ userToDelete.getSupertokensUserId(), UserIdType.SUPERTOKENS);
+ if (mappingResult != null) {
+ primaryUserIdToDeleteNonAuthRecipe = mappingResult.externalUserId;
+ } else {
+ primaryUserIdToDeleteNonAuthRecipe = userToDelete.getSupertokensUserId();
+ }
+
+ }
+ } else {
+ if (userToDelete.getSupertokensUserId().equals(userIdToDeleteForAuthRecipe)) {
+ // this means we are deleting the primary user itself, but keeping other linked accounts
+ // so we keep the non auth recipe info of this user since other linked accounts can use it
+ userIdToDeleteForNonAuthRecipeForRecipeUserId = null;
+ }
+ }
+
+ if (!removeAllLinkedAccounts) {
+ deleteAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForAuthRecipe,
+ !userIdToDeleteForAuthRecipe.equals(userToDelete.getSupertokensUserId()));
+
+ if (userIdToDeleteForNonAuthRecipeForRecipeUserId != null) {
+ deleteNonAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForNonAuthRecipeForRecipeUserId);
+ }
+
+ if (primaryUserIdToDeleteNonAuthRecipe != null) {
+ deleteNonAuthRecipeUser(con, appIdentifierWithStorage, primaryUserIdToDeleteNonAuthRecipe);
+
+ // this is only done to also delete the user ID mapping in case it exists, since we do not delete in the
+ // previous call to deleteAuthRecipeUser above.
+ deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(),
+ true);
+ }
+ } else {
+ for (LoginMethod lM : userToDelete.loginMethods) {
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = lM.getSupertokensUserId().equals(
+ userIdToDeleteForAuthRecipe) ? userIdMapping :
+ io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
+ con,
+ appIdentifierWithStorage,
+ lM.getSupertokensUserId(), UserIdType.SUPERTOKENS);
+ deleteUserHelper(con, appIdentifierWithStorage, lM.getSupertokensUserId(), false, mappingResult);
+ }
}
}
+ @TestOnly
+ public static void deleteUser(Main main, String userId, boolean removeAllLinkedAccounts)
+ throws StorageQueryException, StorageTransactionLogicException {
+ Storage storage = StorageLayer.getStorage(main);
+ AppIdentifierWithStorage appIdentifier = new AppIdentifierWithStorage(
+ null, null, storage);
+ UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifier,
+ userId, UserIdType.ANY);
+
+ deleteUser(appIdentifier, userId, removeAllLinkedAccounts, mapping);
+ }
+
@TestOnly
public static void deleteUser(Main main, String userId)
throws StorageQueryException, StorageTransactionLogicException {
@@ -203,30 +905,37 @@ public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage,
deleteUser(appIdentifierWithStorage, userId, mapping);
}
- private static void deleteNonAuthRecipeUser(AppIdentifierWithStorage
- appIdentifierWithStorage, String userId)
- throws StorageQueryException, StorageTransactionLogicException {
+ private static void deleteNonAuthRecipeUser(TransactionConnection con, AppIdentifierWithStorage
+ appIdentifierWithStorage, String userId)
+ throws StorageQueryException {
appIdentifierWithStorage.getUserMetadataStorage()
- .deleteUserMetadata(appIdentifierWithStorage, userId);
- appIdentifierWithStorage.getSessionStorage()
- .deleteSessionsOfUser(appIdentifierWithStorage, userId);
+ .deleteUserMetadata_Transaction(con, appIdentifierWithStorage, userId);
+ ((SessionSQLStorage) appIdentifierWithStorage.getSessionStorage())
+ .deleteSessionsOfUser_Transaction(con, appIdentifierWithStorage, userId);
appIdentifierWithStorage.getEmailVerificationStorage()
- .deleteEmailVerificationUserInfo(appIdentifierWithStorage, userId);
+ .deleteEmailVerificationUserInfo_Transaction(con, appIdentifierWithStorage, userId);
appIdentifierWithStorage.getUserRolesStorage()
- .deleteAllRolesForUser(appIdentifierWithStorage, userId);
+ .deleteAllRolesForUser_Transaction(con, appIdentifierWithStorage, userId);
appIdentifierWithStorage.getActiveUsersStorage()
- .deleteUserActive(appIdentifierWithStorage, userId);
+ .deleteUserActive_Transaction(con, appIdentifierWithStorage, userId);
+ appIdentifierWithStorage.getTOTPStorage().removeUser_Transaction(con, appIdentifierWithStorage, userId);
+ }
- TOTPSQLStorage storage = appIdentifierWithStorage.getTOTPStorage();
- storage.startTransaction(con -> {
- storage.removeUser_Transaction(con, appIdentifierWithStorage, userId);
- storage.commitTransaction(con);
- return null;
- });
+ private static void deleteAuthRecipeUser(TransactionConnection con,
+ AppIdentifierWithStorage appIdentifierWithStorage, String
+ userId, boolean deleteFromUserIdToAppIdTableToo)
+ throws StorageQueryException {
+ // auth recipe deletions here only
+ appIdentifierWithStorage.getEmailPasswordStorage()
+ .deleteEmailPasswordUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo);
+ appIdentifierWithStorage.getThirdPartyStorage()
+ .deleteThirdPartyUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo);
+ appIdentifierWithStorage.getPasswordlessStorage()
+ .deletePasswordlessUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo);
}
public static boolean deleteNonAuthRecipeUser(TenantIdentifierWithStorage
- tenantIdentifierWithStorage, String userId)
+ tenantIdentifierWithStorage, String userId)
throws StorageQueryException {
// UserMetadata is per app, so nothing to delete
@@ -253,12 +962,13 @@ public static boolean deleteNonAuthRecipeUser(TenantIdentifierWithStorage
return finalDidExist;
}
- private static void deleteAuthRecipeUser(AppIdentifierWithStorage appIdentifierWithStorage, String
- userId)
- throws StorageQueryException {
- // auth recipe deletions here only
- appIdentifierWithStorage.getEmailPasswordStorage().deleteEmailPasswordUser(appIdentifierWithStorage, userId);
- appIdentifierWithStorage.getThirdPartyStorage().deleteThirdPartyUser(appIdentifierWithStorage, userId);
- appIdentifierWithStorage.getPasswordlessStorage().deletePasswordlessUser(appIdentifierWithStorage, userId);
+ private static class UnlinkResult {
+ public final String userId;
+ public final boolean wasLinked;
+
+ public UnlinkResult(String userId, boolean wasLinked) {
+ this.userId = userId;
+ this.wasLinked = wasLinked;
+ }
}
}
diff --git a/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java b/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java
index f1eb37560..d4ef616aa 100644
--- a/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java
+++ b/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java
@@ -22,24 +22,11 @@
import javax.annotation.Nullable;
public class UserPaginationContainer {
- public final UsersContainer[] users;
+ public final AuthRecipeUserInfo[] users;
public final String nextPaginationToken;
public UserPaginationContainer(@Nonnull AuthRecipeUserInfo[] users, @Nullable String nextPaginationToken) {
- this.users = new UsersContainer[users.length];
- for (int i = 0; i < users.length; i++) {
- this.users[i] = new UsersContainer(users[i]);
- }
+ this.users = users;
this.nextPaginationToken = nextPaginationToken;
}
-
- public static class UsersContainer {
- public final AuthRecipeUserInfo user;
- public final String recipeId;
-
- public UsersContainer(AuthRecipeUserInfo user) {
- this.user = user;
- this.recipeId = user.getRecipeId().toString();
- }
- }
}
diff --git a/src/main/java/io/supertokens/emailpassword/UserPaginationContainer.java b/src/main/java/io/supertokens/authRecipe/exception/AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException.java
similarity index 55%
rename from src/main/java/io/supertokens/emailpassword/UserPaginationContainer.java
rename to src/main/java/io/supertokens/authRecipe/exception/AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException.java
index fa112e2dc..7a2805f1e 100644
--- a/src/main/java/io/supertokens/emailpassword/UserPaginationContainer.java
+++ b/src/main/java/io/supertokens/authRecipe/exception/AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
+ * 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.
@@ -14,19 +14,13 @@
* under the License.
*/
-package io.supertokens.emailpassword;
+package io.supertokens.authRecipe.exception;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
+public class AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException extends Exception {
+ public final String primaryUserId;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-public class UserPaginationContainer {
- public final UserInfo[] users;
- public final String nextPaginationToken;
-
- public UserPaginationContainer(@Nonnull UserInfo[] users, @Nullable String nextPaginationToken) {
- this.users = users;
- this.nextPaginationToken = nextPaginationToken;
+ public AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(String primaryUserId, String description) {
+ super(description);
+ this.primaryUserId = primaryUserId;
}
}
diff --git a/src/main/java/io/supertokens/authRecipe/exception/InputUserIdIsNotAPrimaryUserException.java b/src/main/java/io/supertokens/authRecipe/exception/InputUserIdIsNotAPrimaryUserException.java
new file mode 100644
index 000000000..b0fdc333a
--- /dev/null
+++ b/src/main/java/io/supertokens/authRecipe/exception/InputUserIdIsNotAPrimaryUserException.java
@@ -0,0 +1,26 @@
+/*
+ * 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 io.supertokens.authRecipe.exception;
+
+public class InputUserIdIsNotAPrimaryUserException extends Exception {
+ public final String userId;
+
+ public InputUserIdIsNotAPrimaryUserException(String userId) {
+ super();
+ this.userId = userId;
+ }
+}
diff --git a/src/main/java/io/supertokens/thirdparty/UserPaginationContainer.java b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException.java
similarity index 56%
rename from src/main/java/io/supertokens/thirdparty/UserPaginationContainer.java
rename to src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException.java
index cf361ea96..737929a62 100644
--- a/src/main/java/io/supertokens/thirdparty/UserPaginationContainer.java
+++ b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
+ * 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.
@@ -14,19 +14,15 @@
* under the License.
*/
-package io.supertokens.thirdparty;
+package io.supertokens.authRecipe.exception;
-import io.supertokens.pluginInterface.thirdparty.UserInfo;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+public class RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException extends Exception {
+ public final AuthRecipeUserInfo recipeUser;
-public class UserPaginationContainer {
- public final UserInfo[] users;
- public final String nextPaginationToken;
-
- public UserPaginationContainer(@Nonnull UserInfo[] users, @Nullable String nextPaginationToken) {
- this.users = users;
- this.nextPaginationToken = nextPaginationToken;
+ public RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(AuthRecipeUserInfo recipeUser, String description) {
+ super(description);
+ this.recipeUser = recipeUser;
}
}
diff --git a/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithPrimaryUserIdException.java b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithPrimaryUserIdException.java
new file mode 100644
index 000000000..7d0bfe990
--- /dev/null
+++ b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithPrimaryUserIdException.java
@@ -0,0 +1,26 @@
+/*
+ * 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 io.supertokens.authRecipe.exception;
+
+public class RecipeUserIdAlreadyLinkedWithPrimaryUserIdException extends Exception {
+ public final String primaryUserId;
+
+ public RecipeUserIdAlreadyLinkedWithPrimaryUserIdException(String primaryUserId, String description) {
+ super(description);
+ this.primaryUserId = primaryUserId;
+ }
+}
diff --git a/src/main/java/io/supertokens/emailpassword/EmailPassword.java b/src/main/java/io/supertokens/emailpassword/EmailPassword.java
index 362772188..0c7fb08a0 100644
--- a/src/main/java/io/supertokens/emailpassword/EmailPassword.java
+++ b/src/main/java/io/supertokens/emailpassword/EmailPassword.java
@@ -17,16 +17,21 @@
package io.supertokens.emailpassword;
import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
import io.supertokens.emailpassword.exceptions.ResetPasswordInvalidTokenException;
import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException;
import io.supertokens.emailpassword.exceptions.WrongCredentialsException;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.exception.BadPermissionException;
+import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.Storage;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException;
@@ -34,10 +39,14 @@
import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
-import io.supertokens.pluginInterface.multitenancy.*;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.TenantConfig;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.utils.Utils;
+import io.supertokens.webserver.WebserverAPI;
import org.jetbrains.annotations.TestOnly;
import javax.annotation.Nonnull;
@@ -50,9 +59,9 @@ public class EmailPassword {
public static class ImportUserResponse {
public boolean didUserAlreadyExist;
- public UserInfo user;
+ public AuthRecipeUserInfo user;
- public ImportUserResponse(boolean didUserAlreadyExist, UserInfo user) {
+ public ImportUserResponse(boolean didUserAlreadyExist, AuthRecipeUserInfo user) {
this.didUserAlreadyExist = didUserAlreadyExist;
this.user = user;
}
@@ -73,7 +82,7 @@ private static long getPasswordResetTokenLifetime(TenantIdentifier tenantIdentif
}
@TestOnly
- public static UserInfo signUp(Main main, @Nonnull String email, @Nonnull String password)
+ public static AuthRecipeUserInfo signUp(Main main, @Nonnull String email, @Nonnull String password)
throws DuplicateEmailException, StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
@@ -84,7 +93,7 @@ public static UserInfo signUp(Main main, @Nonnull String email, @Nonnull String
}
}
- public static UserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
+ public static AuthRecipeUserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
@Nonnull String email, @Nonnull String password)
throws DuplicateEmailException, StorageQueryException, TenantOrAppNotFoundException,
BadPermissionException {
@@ -106,7 +115,8 @@ public static UserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithSt
long timeJoined = System.currentTimeMillis();
try {
- return tenantIdentifierWithStorage.getEmailPasswordStorage().signUp(tenantIdentifierWithStorage, userId, email, hashedPassword, timeJoined);
+ return tenantIdentifierWithStorage.getEmailPasswordStorage()
+ .signUp(tenantIdentifierWithStorage, userId, email, hashedPassword, timeJoined);
} catch (DuplicateUserIdException ignored) {
// we retry with a new userId (while loop)
@@ -117,7 +127,7 @@ public static UserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithSt
@TestOnly
public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull String email,
@Nonnull String passwordHash, @Nullable
- CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm)
+ CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm)
throws StorageQueryException, StorageTransactionLogicException, UnsupportedPasswordHashingFormatException {
try {
Storage storage = StorageLayer.getStorage(main);
@@ -133,7 +143,7 @@ public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull
public static ImportUserResponse importUserWithPasswordHash(TenantIdentifierWithStorage tenantIdentifierWithStorage,
Main main, @Nonnull String email,
@Nonnull String passwordHash, @Nullable
- CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm)
+ CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm)
throws StorageQueryException, StorageTransactionLogicException, UnsupportedPasswordHashingFormatException,
TenantOrAppNotFoundException, BadPermissionException {
@@ -145,7 +155,8 @@ public static ImportUserResponse importUserWithPasswordHash(TenantIdentifierWith
throw new BadPermissionException("Email password login not enabled for tenant");
}
- PasswordHashingUtils.assertSuperTokensSupportInputPasswordHashFormat(tenantIdentifierWithStorage.toAppIdentifier(), main,
+ PasswordHashingUtils.assertSuperTokensSupportInputPasswordHashFormat(
+ tenantIdentifierWithStorage.toAppIdentifier(), main,
passwordHash, hashingAlgorithm);
while (true) {
@@ -155,18 +166,30 @@ public static ImportUserResponse importUserWithPasswordHash(TenantIdentifierWith
EmailPasswordSQLStorage storage = tenantIdentifierWithStorage.getEmailPasswordStorage();
try {
- UserInfo userInfo = storage.signUp(tenantIdentifierWithStorage, userId, email, passwordHash,
+ AuthRecipeUserInfo userInfo = storage.signUp(tenantIdentifierWithStorage, userId, email, passwordHash,
timeJoined);
return new ImportUserResponse(false, userInfo);
} catch (DuplicateUserIdException e) {
// we retry with a new userId
} catch (DuplicateEmailException e) {
- UserInfo userInfoToBeUpdated = storage.getUserInfoUsingEmail(tenantIdentifierWithStorage, email);
+ AuthRecipeUserInfo[] allUsers = storage.listPrimaryUsersByEmail(tenantIdentifierWithStorage, email);
+ AuthRecipeUserInfo userInfoToBeUpdated = null;
+ LoginMethod loginMethod = null;
+ for (AuthRecipeUserInfo currUser : allUsers) {
+ for (LoginMethod currLM : currUser.loginMethods) {
+ if (currLM.email.equals(email) && currLM.recipeId == RECIPE_ID.EMAIL_PASSWORD && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) {
+ userInfoToBeUpdated = currUser;
+ loginMethod = currLM;
+ break;
+ }
+ }
+ }
if (userInfoToBeUpdated != null) {
+ LoginMethod finalLoginMethod = loginMethod;
storage.startTransaction(con -> {
storage.updateUsersPassword_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con,
- userInfoToBeUpdated.id, passwordHash);
+ finalLoginMethod.getSupertokensUserId(), passwordHash);
return null;
});
return new ImportUserResponse(true, userInfoToBeUpdated);
@@ -190,8 +213,8 @@ public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull
}
@TestOnly
- public static UserInfo signIn(Main main, @Nonnull String email,
- @Nonnull String password)
+ public static AuthRecipeUserInfo signIn(Main main, @Nonnull String email,
+ @Nonnull String password)
throws StorageQueryException, WrongCredentialsException {
try {
Storage storage = StorageLayer.getStorage(main);
@@ -202,8 +225,9 @@ public static UserInfo signIn(Main main, @Nonnull String email,
}
}
- public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, @Nonnull String email,
- @Nonnull String password)
+ public static AuthRecipeUserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
+ @Nonnull String email,
+ @Nonnull String password)
throws StorageQueryException, WrongCredentialsException, TenantOrAppNotFoundException,
BadPermissionException {
@@ -215,8 +239,19 @@ public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithSt
throw new BadPermissionException("Email password login not enabled for tenant");
}
- UserInfo user = tenantIdentifierWithStorage.getEmailPasswordStorage()
- .getUserInfoUsingEmail(tenantIdentifierWithStorage, email);
+ AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getAuthRecipeStorage()
+ .listPrimaryUsersByEmail(tenantIdentifierWithStorage, email);
+
+ AuthRecipeUserInfo user = null;
+ LoginMethod lM = null;
+ for (AuthRecipeUserInfo currUser : users) {
+ for (LoginMethod currLM : currUser.loginMethods) {
+ if (currLM.recipeId == RECIPE_ID.EMAIL_PASSWORD && currLM.email.equals(email) && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) {
+ user = currUser;
+ lM = currLM;
+ }
+ }
+ }
if (user == null) {
throw new WrongCredentialsException();
@@ -224,7 +259,8 @@ public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithSt
try {
if (!PasswordHashing.getInstance(main)
- .verifyPasswordWithHash(tenantIdentifierWithStorage.toAppIdentifier(), password, user.passwordHash)) {
+ .verifyPasswordWithHash(tenantIdentifierWithStorage.toAppIdentifier(), password,
+ lM.passwordHash)) {
throw new WrongCredentialsException();
}
} catch (WrongCredentialsException e) {
@@ -243,20 +279,68 @@ public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithSt
}
@TestOnly
- public static String generatePasswordResetToken(Main main, String userId)
+ public static String generatePasswordResetTokenBeforeCdi4_0(Main main, String userId)
throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException {
try {
Storage storage = StorageLayer.getStorage(main);
- return generatePasswordResetToken(
+ return generatePasswordResetTokenBeforeCdi4_0(
new TenantIdentifierWithStorage(null, null, null, storage),
main, userId);
+ } catch (TenantOrAppNotFoundException | BadPermissionException | WebserverAPI.BadRequestException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @TestOnly
+ public static String generatePasswordResetTokenBeforeCdi4_0WithoutAddingEmail(Main main, String userId)
+ throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException {
+ try {
+ Storage storage = StorageLayer.getStorage(main);
+ return generatePasswordResetToken(
+ new TenantIdentifierWithStorage(null, null, null, storage),
+ main, userId, null);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
}
}
+ @TestOnly
+ public static String generatePasswordResetToken(Main main, String userId, String email)
+ throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException {
+ try {
+ Storage storage = StorageLayer.getStorage(main);
+ return generatePasswordResetToken(
+ new TenantIdentifierWithStorage(null, null, null, storage),
+ main, userId, email);
+ } catch (TenantOrAppNotFoundException | BadPermissionException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static String generatePasswordResetTokenBeforeCdi4_0(TenantIdentifierWithStorage tenantIdentifierWithStorage,
+ Main main,
+ String userId)
+ throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException,
+ TenantOrAppNotFoundException, BadPermissionException, WebserverAPI.BadRequestException {
+ AppIdentifierWithStorage appIdentifierWithStorage =
+ tenantIdentifierWithStorage.toAppIdentifierWithStorage();
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(appIdentifierWithStorage, userId);
+ if (user == null) {
+ throw new UnknownUserIdException();
+ }
+ if (user.loginMethods.length > 1) {
+ throw new WebserverAPI.BadRequestException("Please use CDI version >= 4.0");
+ }
+ if (user.loginMethods[0].email == null ||
+ user.loginMethods[0].recipeId != RECIPE_ID.EMAIL_PASSWORD) {
+ // this used to be the behaviour of the older CDI version and it was enforced via a fkey constraint
+ throw new UnknownUserIdException();
+ }
+ return generatePasswordResetToken(tenantIdentifierWithStorage, main, userId, user.loginMethods[0].email);
+ }
+
public static String generatePasswordResetToken(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
- String userId)
+ String userId, String email)
throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException,
TenantOrAppNotFoundException, BadPermissionException {
@@ -293,7 +377,7 @@ public static String generatePasswordResetToken(TenantIdentifierWithStorage tena
tenantIdentifierWithStorage.getEmailPasswordStorage().addPasswordResetToken(
tenantIdentifierWithStorage.toAppIdentifier(), new PasswordResetTokenInfo(userId,
hashedToken, System.currentTimeMillis() +
- getPasswordResetTokenLifetime(tenantIdentifierWithStorage, main)));
+ getPasswordResetTokenLifetime(tenantIdentifierWithStorage, main), email));
return token;
} catch (DuplicatePasswordResetTokenException ignored) {
}
@@ -301,6 +385,7 @@ public static String generatePasswordResetToken(TenantIdentifierWithStorage tena
}
@TestOnly
+ @Deprecated
public static String resetPassword(Main main, String token,
String password)
throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException,
@@ -314,6 +399,7 @@ public static String resetPassword(Main main, String token,
}
}
+ @Deprecated
public static String resetPassword(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String token,
String password)
throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException,
@@ -351,7 +437,8 @@ public static String resetPassword(TenantIdentifierWithStorage tenantIdentifierW
throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException());
}
- storage.deleteAllPasswordResetTokensForUser_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con,
+ storage.deleteAllPasswordResetTokensForUser_Transaction(tenantIdentifierWithStorage.toAppIdentifier(),
+ con,
userId);
if (matchedToken.tokenExpiry < System.currentTimeMillis()) {
@@ -373,12 +460,109 @@ public static String resetPassword(TenantIdentifierWithStorage tenantIdentifierW
}
}
+ @TestOnly
+ public static ConsumeResetPasswordTokenResult consumeResetPasswordToken(Main main, String token)
+ throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException,
+ StorageTransactionLogicException {
+ try {
+ Storage storage = StorageLayer.getStorage(main);
+ return consumeResetPasswordToken(new TenantIdentifierWithStorage(null, null, null, storage),
+ token);
+ } catch (TenantOrAppNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static class ConsumeResetPasswordTokenResult {
+ public String userId;
+ public String email;
+
+ public ConsumeResetPasswordTokenResult(String userId, String email) {
+ this.userId = userId;
+ this.email = email;
+ }
+ }
+
+ public static ConsumeResetPasswordTokenResult consumeResetPasswordToken(
+ TenantIdentifierWithStorage tenantIdentifierWithStorage, String token)
+ throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException,
+ StorageTransactionLogicException, TenantOrAppNotFoundException {
+ String hashedToken = Utils.hashSHA256(token);
+
+ EmailPasswordSQLStorage storage = tenantIdentifierWithStorage.getEmailPasswordStorage();
+
+ PasswordResetTokenInfo resetInfo = storage.getPasswordResetTokenInfo(
+ tenantIdentifierWithStorage.toAppIdentifier(), hashedToken);
+
+ if (resetInfo == null) {
+ throw new ResetPasswordInvalidTokenException();
+ }
+
+ final String userId = resetInfo.userId;
+
+ try {
+ return storage.startTransaction(con -> {
+
+ PasswordResetTokenInfo[] allTokens = storage.getAllPasswordResetTokenInfoForUser_Transaction(
+ tenantIdentifierWithStorage.toAppIdentifier(), con,
+ userId);
+
+ PasswordResetTokenInfo matchedToken = null;
+ for (PasswordResetTokenInfo tok : allTokens) {
+ if (tok.token.equals(hashedToken)) {
+ matchedToken = tok;
+ break;
+ }
+ }
+
+ if (matchedToken == null) {
+ throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException());
+ }
+
+ storage.deleteAllPasswordResetTokensForUser_Transaction(tenantIdentifierWithStorage.toAppIdentifier(),
+ con,
+ userId);
+
+ if (matchedToken.tokenExpiry < System.currentTimeMillis()) {
+ storage.commitTransaction(con);
+ throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException());
+ }
+
+ storage.commitTransaction(con);
+ if (matchedToken.email == null) {
+ // this is possible if the token was generated before migration, and then consumed
+ // after migration
+ AppIdentifierWithStorage appIdentifierWithStorage =
+ tenantIdentifierWithStorage.toAppIdentifierWithStorage();
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(appIdentifierWithStorage, userId);
+ if (user == null) {
+ throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException());
+ }
+ if (user.loginMethods.length > 1) {
+ throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException());
+ }
+ if (user.loginMethods[0].email == null ||
+ user.loginMethods[0].recipeId != RECIPE_ID.EMAIL_PASSWORD) {
+ throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException());
+ }
+ return new ConsumeResetPasswordTokenResult(matchedToken.userId, user.loginMethods[0].email);
+ }
+ return new ConsumeResetPasswordTokenResult(matchedToken.userId, matchedToken.email);
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof ResetPasswordInvalidTokenException) {
+ throw (ResetPasswordInvalidTokenException) e.actualException;
+ }
+ throw e;
+ }
+ }
+
@TestOnly
public static void updateUsersEmailOrPassword(Main main,
@Nonnull String userId, @Nullable String email,
@Nullable String password)
throws StorageQueryException, StorageTransactionLogicException,
- UnknownUserIdException, DuplicateEmailException {
+ UnknownUserIdException, DuplicateEmailException, EmailChangeNotAllowedException {
try {
Storage storage = StorageLayer.getStorage(main);
updateUsersEmailOrPassword(new AppIdentifierWithStorage(null, null, storage),
@@ -392,18 +576,50 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti
@Nonnull String userId, @Nullable String email,
@Nullable String password)
throws StorageQueryException, StorageTransactionLogicException,
- UnknownUserIdException, DuplicateEmailException, TenantOrAppNotFoundException {
+ UnknownUserIdException, DuplicateEmailException, TenantOrAppNotFoundException,
+ EmailChangeNotAllowedException {
EmailPasswordSQLStorage storage = appIdentifierWithStorage.getEmailPasswordStorage();
+ AuthRecipeSQLStorage authRecipeStorage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
try {
storage.startTransaction(transaction -> {
try {
- UserInfo userInfo = storage.getUserInfoUsingId_Transaction(appIdentifierWithStorage, transaction, userId);
+ AuthRecipeUserInfo user = authRecipeStorage.getPrimaryUserById_Transaction(appIdentifierWithStorage,
+ transaction, userId);
- if (userInfo == null) {
+ if (user == null) {
+ throw new StorageTransactionLogicException(new UnknownUserIdException());
+ }
+ boolean foundEmailPasswordLoginMethod = false;
+ for (LoginMethod lm : user.loginMethods) {
+ if (lm.recipeId == RECIPE_ID.EMAIL_PASSWORD && lm.getSupertokensUserId().equals(userId)) {
+ foundEmailPasswordLoginMethod = true;
+ break;
+ }
+ }
+ if (!foundEmailPasswordLoginMethod) {
throw new StorageTransactionLogicException(new UnknownUserIdException());
}
if (email != null) {
+ if (user.isPrimaryUser) {
+ for (String tenantId : user.tenantIds) {
+ AuthRecipeUserInfo[] existingUsersWithNewEmail =
+ authRecipeStorage.listPrimaryUsersByEmail_Transaction(
+ appIdentifierWithStorage, transaction,
+ email);
+
+ for (AuthRecipeUserInfo userWithSameEmail : existingUsersWithNewEmail) {
+ if (!userWithSameEmail.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(user.getSupertokensUserId())) {
+ throw new StorageTransactionLogicException(
+ new EmailChangeNotAllowedException());
+ }
+ }
+ }
+ }
+
try {
storage.updateUsersEmail_Transaction(appIdentifierWithStorage, transaction,
userId, email);
@@ -415,7 +631,8 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti
if (password != null) {
String hashedPassword = PasswordHashing.getInstance(main)
.createHashWithSalt(appIdentifierWithStorage, password);
- storage.updateUsersPassword_Transaction(appIdentifierWithStorage, transaction, userId, hashedPassword);
+ storage.updateUsersPassword_Transaction(appIdentifierWithStorage, transaction, userId,
+ hashedPassword);
}
storage.commitTransaction(transaction);
@@ -431,13 +648,16 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti
throw (DuplicateEmailException) e.actualException;
} else if (e.actualException instanceof TenantOrAppNotFoundException) {
throw (TenantOrAppNotFoundException) e.actualException;
+ } else if (e.actualException instanceof EmailChangeNotAllowedException) {
+ throw (EmailChangeNotAllowedException) e.actualException;
}
throw e;
}
}
+ @Deprecated
@TestOnly
- public static UserInfo getUserUsingId(Main main, String userId)
+ public static AuthRecipeUserInfo getUserUsingId(Main main, String userId)
throws StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
@@ -447,14 +667,36 @@ public static UserInfo getUserUsingId(Main main, String userId)
}
}
- public static UserInfo getUserUsingId(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
+ @Deprecated
+ public static AuthRecipeUserInfo getUserUsingId(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
throws StorageQueryException, TenantOrAppNotFoundException {
- return appIdentifierWithStorage.getEmailPasswordStorage().getUserInfoUsingId(appIdentifierWithStorage, userId);
+ AuthRecipeUserInfo result = appIdentifierWithStorage.getAuthRecipeStorage()
+ .getPrimaryUserById(appIdentifierWithStorage, userId);
+ if (result == null) {
+ return null;
+ }
+ for (LoginMethod lM : result.loginMethods) {
+ if (lM.getSupertokensUserId().equals(userId) && lM.recipeId == RECIPE_ID.EMAIL_PASSWORD) {
+ return AuthRecipeUserInfo.create(lM.getSupertokensUserId(), result.isPrimaryUser, lM);
+ }
+ }
+ return null;
}
- public static UserInfo getUserUsingEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, String email)
+ @Deprecated
+ public static AuthRecipeUserInfo getUserUsingEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage,
+ String email)
throws StorageQueryException, TenantOrAppNotFoundException {
- return tenantIdentifierWithStorage.getEmailPasswordStorage().getUserInfoUsingEmail(
+ AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getAuthRecipeStorage().listPrimaryUsersByEmail(
tenantIdentifierWithStorage, email);
+ // filter used based on login method
+ for (AuthRecipeUserInfo user : users) {
+ for (LoginMethod lM : user.loginMethods) {
+ if (lM.email.equals(email) && lM.recipeId == RECIPE_ID.EMAIL_PASSWORD) {
+ return user;
+ }
+ }
+ }
+ return null;
}
}
diff --git a/src/main/java/io/supertokens/emailpassword/exceptions/EmailChangeNotAllowedException.java b/src/main/java/io/supertokens/emailpassword/exceptions/EmailChangeNotAllowedException.java
new file mode 100644
index 000000000..e8660b592
--- /dev/null
+++ b/src/main/java/io/supertokens/emailpassword/exceptions/EmailChangeNotAllowedException.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2020, 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 io.supertokens.emailpassword.exceptions;
+
+public class EmailChangeNotAllowedException extends Exception {
+ private static final long serialVersionUID = -7205953190075543040L;
+}
diff --git a/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java b/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java
index a4c2ddfef..700793cf7 100644
--- a/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java
+++ b/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java
@@ -21,10 +21,12 @@ public class TokenTheftDetectedException extends Exception {
private static final long serialVersionUID = -7964000536695705071L;
public final String sessionHandle;
- public final String userId;
+ public final String recipeUserId;
+ public final String primaryUserId;
- public TokenTheftDetectedException(String sessionHandle, String userId) {
+ public TokenTheftDetectedException(String sessionHandle, String recipeUserId, String primaryUserId) {
this.sessionHandle = sessionHandle;
- this.userId = userId;
+ this.recipeUserId = recipeUserId;
+ this.primaryUserId = primaryUserId;
}
}
diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java
index 43be8c364..f05d07f8a 100644
--- a/src/main/java/io/supertokens/inmemorydb/Start.java
+++ b/src/main/java/io/supertokens/inmemorydb/Start.java
@@ -24,13 +24,14 @@
import io.supertokens.inmemorydb.queries.*;
import io.supertokens.pluginInterface.*;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo;
import io.supertokens.pluginInterface.dashboard.DashboardUser;
import io.supertokens.pluginInterface.dashboard.exceptions.UserIdNotFoundException;
import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage;
import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException;
@@ -53,6 +54,7 @@
import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException;
import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage;
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
import io.supertokens.pluginInterface.passwordless.exception.*;
@@ -75,6 +77,7 @@
import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage;
import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException;
import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException;
+import io.supertokens.pluginInterface.useridmapping.sqlStorage.UserIdMappingSQLStorage;
import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage;
import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage;
import io.supertokens.pluginInterface.userroles.UserRolesStorage;
@@ -90,12 +93,16 @@
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLTransactionRollbackException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
public class Start
implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage,
JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage,
- MultitenancyStorage, TOTPSQLStorage, ActiveUsersStorage, DashboardSQLStorage {
+ UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage,
+ DashboardSQLStorage, AuthRecipeSQLStorage {
private static final Object appenderLock = new Object();
private static final String APP_ID_KEY_NAME = "app_id";
@@ -134,7 +141,8 @@ public STORAGE_TYPE getType() {
}
@Override
- public void loadConfig(JsonObject ignored, Set logLevel, TenantIdentifier tenantIdentifier) throws InvalidConfigException {
+ public void loadConfig(JsonObject ignored, Set logLevel, TenantIdentifier tenantIdentifier)
+ throws InvalidConfigException {
Config.loadConfig(this);
}
@@ -248,7 +256,8 @@ public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(AppIdentifier app
throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return GeneralQueries.getKeyValue_Transaction(this, sqlCon, appIdentifier.getAsPublicTenantIdentifier(), ACCESS_TOKEN_SIGNING_KEY_NAME);
+ return GeneralQueries.getKeyValue_Transaction(this, sqlCon, appIdentifier.getAsPublicTenantIdentifier(),
+ ACCESS_TOKEN_SIGNING_KEY_NAME);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -448,7 +457,7 @@ public KeyValueInfo getKeyValue(TenantIdentifier tenantIdentifier, String key) t
}
}
- @Override
+ @Override
public void setKeyValue(TenantIdentifier tenantIdentifier, String key, KeyValueInfo info)
throws StorageQueryException, TenantOrAppNotFoundException {
try {
@@ -514,7 +523,19 @@ public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, Tra
long expiry) throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, refreshTokenHash2, expiry);
+ SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle,
+ refreshTokenHash2, expiry);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public void deleteSessionsOfUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
+ Connection sqlCon = (Connection) con.getConnection();
+ try {
+ SessionQueries.deleteSessionsOfUser_Transaction(sqlCon, this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -602,7 +623,8 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str
@TestOnly
@Override
- public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) throws StorageQueryException {
+ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId)
+ throws StorageQueryException {
if (!isTesting) {
throw new UnsupportedOperationException();
}
@@ -672,7 +694,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi
try {
long now = System.currentTimeMillis();
TOTPQueries.insertUsedCode_Transaction(this,
- (Connection) con.getConnection(), tenantIdentifier, new TOTPUsedCode(userId, "123456", true, 1000+now, now));
+ (Connection) con.getConnection(), tenantIdentifier,
+ new TOTPUsedCode(userId, "123456", true, 1000 + now, now));
} catch (SQLException e) {
throw new StorageTransactionLogicException(e);
}
@@ -706,7 +729,8 @@ public String[] getProtectedConfigsFromSuperTokensSaaSUsers() {
}
@Override
- public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, long timeJoined)
+ public AuthRecipeUserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash,
+ long timeJoined)
throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException,
TenantOrAppNotFoundException {
try {
@@ -716,12 +740,17 @@ public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String emai
SQLiteConfig config = Config.getConfig(this);
String serverMessage = eTemp.actualException.getMessage();
- if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) {
+ if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateEmailException();
- } else if (isPrimaryKeyError(serverMessage, config.getEmailPasswordUsersTable(), new String[]{"app_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getUsersTable(), new String[]{"app_id", "tenant_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), new String[]{"app_id", "user_id"})) {
+ } else if (isPrimaryKeyError(serverMessage, config.getEmailPasswordUsersTable(),
+ new String[]{"app_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getUsersTable(),
+ new String[]{"app_id", "tenant_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
+ new String[]{"app_id", "user_id"})) {
throw new DuplicateUserIdException();
} else if (isForeignKeyConstraintError(
serverMessage,
@@ -742,45 +771,18 @@ public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String emai
}
}
- @Override
- public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
- try {
- EmailPasswordQueries.deleteUser(this, appIdentifier, userId);
- } catch (StorageTransactionLogicException e) {
- throw new StorageQueryException(e.actualException);
- }
- }
-
- @Override
- public UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException {
- try {
- return EmailPasswordQueries.getUserInfoUsingId(this, appIdentifier, id);
- } catch (SQLException e) {
- throw new StorageQueryException(e);
- }
- }
-
- @Override
- public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String email)
- throws StorageQueryException {
- try {
- return EmailPasswordQueries.getUserInfoUsingEmail(this, tenantIdentifier, email);
- } catch (SQLException e) {
- throw new StorageQueryException(e);
- }
- }
-
@Override
public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetTokenInfo passwordResetTokenInfo)
throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException {
try {
EmailPasswordQueries.addPasswordResetToken(this, appIdentifier, passwordResetTokenInfo.userId,
- passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry);
+ passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry, passwordResetTokenInfo.email);
} catch (SQLException e) {
if (e instanceof SQLiteException) {
String serverMessage = e.getMessage();
- if (isPrimaryKeyError(serverMessage, Config.getConfig(this).getPasswordResetTokensTable(), new String[]{"app_id", "user_id", "token"})) {
+ if (isPrimaryKeyError(serverMessage, Config.getConfig(this).getPasswordResetTokensTable(),
+ new String[]{"app_id", "user_id", "token"})) {
throw new DuplicatePasswordResetTokenException();
} else if (isForeignKeyConstraintError(
serverMessage,
@@ -861,7 +863,8 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio
EmailPasswordQueries.updateUsersEmail_Transaction(this, sqlCon, appIdentifier, userId, email);
} catch (SQLException e) {
if (isUniqueConstraintError(e.getMessage(),
- Config.getConfig(this).getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) {
+ Config.getConfig(this).getEmailPasswordUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateEmailException();
}
throw new StorageQueryException(e);
@@ -869,12 +872,12 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio
}
@Override
- public UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
- String userId)
+ public void deleteEmailPasswordUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier,
+ String userId, boolean deleteUserIdMappingToo)
throws StorageQueryException {
- Connection sqlCon = (Connection) con.getConnection();
try {
- return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, userId);
+ Connection sqlCon = (Connection) con.getConnection();
+ EmailPasswordQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -953,12 +956,13 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans
}
@Override
- public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId)
- throws StorageQueryException {
+ public void deleteEmailVerificationUserInfo_Transaction(TransactionConnection con, AppIdentifier appIdentifier,
+ String userId) throws StorageQueryException {
try {
- EmailVerificationQueries.deleteUserInfo(this, appIdentifier, userId);
- } catch (StorageTransactionLogicException e) {
- throw new StorageQueryException(e.actualException);
+ Connection sqlCon = (Connection) con.getConnection();
+ EmailVerificationQueries.deleteUserInfo_Transaction(sqlCon, this, appIdentifier, userId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
}
}
@@ -984,7 +988,8 @@ public void addEmailVerificationToken(TenantIdentifier tenantIdentifier,
SQLiteConfig config = Config.getConfig(this);
String serverMessage = e.getMessage();
- if (isPrimaryKeyError(serverMessage, config.getEmailVerificationTokensTable(), new String[]{"app_id", "tenant_id", "user_id", "email", "token"})) {
+ if (isPrimaryKeyError(serverMessage, config.getEmailVerificationTokensTable(),
+ new String[]{"app_id", "tenant_id", "user_id", "email", "token"})) {
throw new DuplicateEmailVerificationTokenException();
}
@@ -1063,37 +1068,34 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException {
}
@Override
- public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction(
- AppIdentifier appIdentifier, TransactionConnection con,
- String thirdPartyId,
- String thirdPartyUserId)
- throws StorageQueryException {
+ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
+ String thirdPartyId, String thirdPartyUserId,
+ String newEmail) throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, thirdPartyId,
- thirdPartyUserId);
+ ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, thirdPartyId,
+ thirdPartyUserId, newEmail);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
- String thirdPartyId, String thirdPartyUserId,
- String newEmail) throws StorageQueryException {
- Connection sqlCon = (Connection) con.getConnection();
+ public void deleteThirdPartyUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId,
+ boolean deleteUserIdMappingToo)
+ throws StorageQueryException {
try {
- ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, thirdPartyId,
- thirdPartyUserId, newEmail);
+ Connection sqlCon = (Connection) con.getConnection();
+ ThirdPartyQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public io.supertokens.pluginInterface.thirdparty.UserInfo signUp(
+ public AuthRecipeUserInfo signUp(
TenantIdentifier tenantIdentifier, String id, String email,
- io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty thirdParty, long timeJoined)
+ LoginMethod.ThirdParty thirdParty, long timeJoined)
throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException,
DuplicateThirdPartyUserException, TenantOrAppNotFoundException {
try {
@@ -1107,10 +1109,14 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp(
new String[]{"app_id", "tenant_id", "third_party_id", "third_party_user_id"})) {
throw new DuplicateThirdPartyUserException();
- } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable(), new String[]{"app_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getUsersTable(), new String[]{"app_id", "tenant_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable(), new String[]{"app_id", "tenant_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), new String[]{"app_id", "user_id"})) {
+ } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable(),
+ new String[]{"app_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getUsersTable(),
+ new String[]{"app_id", "tenant_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
+ new String[]{"app_id", "user_id"})) {
throw new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException();
} else if (isForeignKeyConstraintError(
@@ -1134,143 +1140,162 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp(
}
@Override
- public void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
+ public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds)
+ throws StorageQueryException {
try {
- ThirdPartyQueries.deleteUser(this, appIdentifier, userId);
- } catch (StorageTransactionLogicException e) {
- throw new StorageQueryException(e.actualException);
+ return GeneralQueries.getUsersCount(this, tenantIdentifier, includeRecipeIds);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
}
}
@Override
- public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(
- TenantIdentifier tenantIdentifier, String thirdPartyId,
- String thirdPartyUserId)
+ public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds)
throws StorageQueryException {
try {
- return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId,
- thirdPartyUserId);
+ return GeneralQueries.getUsersCount(this, appIdentifier, includeRecipeIds);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier,
- String id)
+ public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit,
+ @NotNull String timeJoinedOrder,
+ @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId,
+ @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags)
throws StorageQueryException {
try {
- return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, appIdentifier, id);
+ return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId,
+ timeJoined, dashboardSearchTags);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail(
- TenantIdentifier tenantIdentifier, @NotNull String email)
- throws StorageQueryException {
+ public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
try {
- return ThirdPartyQueries.getThirdPartyUsersByEmail(this, tenantIdentifier, email);
+ return GeneralQueries.doesUserIdExist(this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds)
- throws StorageQueryException {
+ public boolean doesUserIdExist_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException {
try {
- return GeneralQueries.getUsersCount(this, tenantIdentifier, includeRecipeIds);
+ Connection sqlCon = (Connection) con.getConnection();
+ return GeneralQueries.doesUserIdExist_Transaction(this, sqlCon, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds)
- throws StorageQueryException {
+ public void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
try {
- return GeneralQueries.getUsersCount(this, appIdentifier, includeRecipeIds);
+ ActiveUsersQueries.updateUserLastActive(this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit,
- @NotNull String timeJoinedOrder,
- @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId,
- @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags)
- throws StorageQueryException {
+ public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException {
try {
- return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId,
- timeJoined, dashboardSearchTags);
+ return ActiveUsersQueries.countUsersActiveSince(this, appIdentifier, time);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
+ public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException {
try {
- return GeneralQueries.doesUserIdExist(this, appIdentifier, userId);
+ return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
+ public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time)
+ throws StorageQueryException {
try {
- ActiveUsersQueries.updateUserLastActive(this, appIdentifier, userId);
+ return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException {
+ public void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
try {
- return ActiveUsersQueries.countUsersActiveSince(this, appIdentifier, time);
+ Connection sqlCon = (Connection) con.getConnection();
+ ActiveUsersQueries.deleteUserActive_Transaction(sqlCon, this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException {
+ public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId)
+ throws StorageQueryException {
try {
- return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier);
+ return GeneralQueries.doesUserIdExist(this, tenantIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time)
+ public AuthRecipeUserInfo getPrimaryUserById(AppIdentifier appIdentifier, String userId)
throws StorageQueryException {
try {
- return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time);
+ return GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
+ public String getPrimaryUserIdStrForUserId(AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
try {
- ActiveUsersQueries.deleteUserActive(this, appIdentifier, userId);
+ return GeneralQueries.getPrimaryUserIdStrForUserId(this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId)
+ public AuthRecipeUserInfo[] listPrimaryUsersByEmail(TenantIdentifier tenantIdentifier, String email)
throws StorageQueryException {
try {
- return GeneralQueries.doesUserIdExist(this, tenantIdentifier, userId);
+ return GeneralQueries.listPrimaryUsersByEmail(this, tenantIdentifier, email);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber)
+ throws StorageQueryException {
+ try {
+ return GeneralQueries.listPrimaryUsersByPhoneNumber(this, tenantIdentifier, phoneNumber);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(TenantIdentifier tenantIdentifier, String thirdPartyId,
+ String thirdPartyUserId) throws StorageQueryException {
+ try {
+ return GeneralQueries.getPrimaryUserByThirdPartyInfo(this, tenantIdentifier, thirdPartyId,
+ thirdPartyUserId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1300,7 +1325,8 @@ public void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, Transactio
SQLiteConfig config = Config.getConfig(this);
String serverMessage = e.getMessage();
- if (isPrimaryKeyError(serverMessage, config.getJWTSigningKeysTable(), new String[]{"app_id", "key_id"})) {
+ if (isPrimaryKeyError(serverMessage, config.getJWTSigningKeysTable(),
+ new String[]{"app_id", "key_id"})) {
throw new DuplicateKeyIdException();
}
@@ -1322,7 +1348,8 @@ private boolean isUniqueConstraintError(String serverMessage, String tableName,
return isPrimaryKeyError(serverMessage, tableName, columnNames);
}
- private boolean isForeignKeyConstraintError(String serverMessage, String tableName, String[] columnNames, Object[] values) {
+ private boolean isForeignKeyConstraintError(String serverMessage, String tableName, String[] columnNames,
+ Object[] values) {
if (!serverMessage.contains("FOREIGN KEY constraint failed")) {
return false;
}
@@ -1496,7 +1523,8 @@ public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, Transactio
}
@Override
- public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String email)
+ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId,
+ String email)
throws StorageQueryException, UnknownUserIdException, DuplicateEmailException {
Connection sqlCon = (Connection) con.getConnection();
try {
@@ -1508,7 +1536,8 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction
} catch (SQLException e) {
if (e instanceof SQLiteException) {
if (isUniqueConstraintError(e.getMessage(),
- Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) {
+ Config.getConfig(this).getPasswordlessUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateEmailException();
}
}
@@ -1533,7 +1562,8 @@ public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, Trans
} catch (SQLException e) {
if (e instanceof SQLiteException) {
if (isUniqueConstraintError(e.getMessage(),
- Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "phone_number"})) {
+ Config.getConfig(this).getPasswordlessUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "phone_number"})) {
throw new DuplicatePhoneNumberException();
}
}
@@ -1542,6 +1572,18 @@ public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, Trans
}
}
+ @Override
+ public void deletePasswordlessUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier,
+ String userId, boolean deleteUserIdMappingToo)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ PasswordlessQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
@Override
public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable String email,
@Nullable String phoneNumber, @NotNull String linkCodeSalt,
@@ -1559,13 +1601,16 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St
String serverMessage = e.actualException.getMessage();
SQLiteConfig config = Config.getConfig(this);
- if (isPrimaryKeyError(serverMessage, config.getPasswordlessDevicesTable(), new String[]{"app_id", "tenant_id", "device_id_hash"})) {
+ if (isPrimaryKeyError(serverMessage, config.getPasswordlessDevicesTable(),
+ new String[]{"app_id", "tenant_id", "device_id_hash"})) {
throw new DuplicateDeviceIdHashException();
}
- if (isPrimaryKeyError(serverMessage, config.getPasswordlessCodesTable(), new String[]{"app_id", "tenant_id", "code_id"})) {
+ if (isPrimaryKeyError(serverMessage, config.getPasswordlessCodesTable(),
+ new String[]{"app_id", "tenant_id", "code_id"})) {
throw new DuplicateCodeIdException();
}
- if (isUniqueConstraintError(serverMessage, config.getPasswordlessCodesTable(), new String[]{"app_id", "tenant_id", "link_code_hash"})) {
+ if (isUniqueConstraintError(serverMessage, config.getPasswordlessCodesTable(),
+ new String[]{"app_id", "tenant_id", "link_code_hash"})) {
throw new DuplicateLinkCodeHashException();
}
if (isForeignKeyConstraintError(
@@ -1582,7 +1627,8 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St
}
@Override
- public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) throws StorageQueryException, UnknownDeviceIdHash,
+ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code)
+ throws StorageQueryException, UnknownDeviceIdHash,
DuplicateCodeIdException, DuplicateLinkCodeHashException {
try {
@@ -1614,9 +1660,11 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code)
}
@Override
- public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIdentifier tenantIdentifier,
- String id, @javax.annotation.Nullable String email,
- @javax.annotation.Nullable String phoneNumber, long timeJoined)
+ public AuthRecipeUserInfo createUser(TenantIdentifier tenantIdentifier,
+ String id,
+ @javax.annotation.Nullable String email,
+ @javax.annotation.Nullable
+ String phoneNumber, long timeJoined)
throws StorageQueryException,
DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException,
TenantOrAppNotFoundException {
@@ -1627,10 +1675,14 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde
SQLiteConfig config = Config.getConfig(this);
String serverMessage = e.actualException.getMessage();
- if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable(), new String[]{"app_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getUsersTable(), new String[]{"app_id", "tenant_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "user_id"})
- || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), new String[]{"app_id", "user_id"})) {
+ if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable(),
+ new String[]{"app_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getUsersTable(),
+ new String[]{"app_id", "tenant_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "user_id"})
+ || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
+ new String[]{"app_id", "user_id"})) {
throw new DuplicateUserIdException();
}
@@ -1640,7 +1692,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde
}
if (isUniqueConstraintError(serverMessage,
- config.getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "phone_number"})) {
+ config.getPasswordlessUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "phone_number"})) {
throw new DuplicatePhoneNumberException();
}
@@ -1665,16 +1718,6 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde
}
}
- @Override
- public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws
- StorageQueryException {
- try {
- PasswordlessQueries.deleteUser(this, appIdentifier, userId);
- } catch (StorageTransactionLogicException e) {
- throw new StorageQueryException(e.actualException);
- }
- }
-
@Override
public PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String deviceIdHash)
throws StorageQueryException {
@@ -1745,36 +1788,6 @@ public PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier,
}
}
- @Override
- public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdentifier appIdentifier, String userId)
- throws StorageQueryException {
- try {
- return PasswordlessQueries.getUserById(this, appIdentifier, userId);
- } catch (SQLException e) {
- throw new StorageQueryException(e);
- }
- }
-
- @Override
- public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, String email)
- throws StorageQueryException {
- try {
- return PasswordlessQueries.getUserByEmail(this, tenantIdentifier, email);
- } catch (SQLException e) {
- throw new StorageQueryException(e);
- }
- }
-
- @Override
- public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber)
- throws StorageQueryException {
- try {
- return PasswordlessQueries.getUserByPhoneNumber(this, tenantIdentifier, phoneNumber);
- } catch (SQLException e) {
- throw new StorageQueryException(e);
- }
- }
-
@Override
public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
try {
@@ -1797,7 +1810,8 @@ public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, Trans
}
@Override
- public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, JsonObject metadata)
+ public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId,
+ JsonObject metadata)
throws StorageQueryException, TenantOrAppNotFoundException {
Connection sqlCon = (Connection) con.getConnection();
try {
@@ -1820,6 +1834,17 @@ public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionC
}
}
+ @Override
+ public int deleteUserMetadata_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ return UserMetadataQueries.deleteUserMetadata_Transaction(sqlCon, this, appIdentifier, userId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
@Override
public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
try {
@@ -1834,7 +1859,7 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri
throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException,
TenantOrAppNotFoundException {
try {
- UserRoleQueries.addRoleToUser(this, tenantIdentifier, userId, role);
+ UserRolesQueries.addRoleToUser(this, tenantIdentifier, userId, role);
} catch (SQLException e) {
if (e instanceof SQLiteException) {
SQLiteConfig config = Config.getConfig(this);
@@ -1847,7 +1872,8 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri
new Object[]{tenantIdentifier.getAppId(), role})) {
throw new UnknownRoleException();
}
- if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable(), new String[]{"app_id", "tenant_id", "user_id", "role"})) {
+ if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable(),
+ new String[]{"app_id", "tenant_id", "user_id", "role"})) {
throw new DuplicateUserRoleMappingException();
}
if (isForeignKeyConstraintError(
@@ -1867,7 +1893,7 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri
public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws
StorageQueryException {
try {
- return UserRoleQueries.getRolesForUser(this, tenantIdentifier, userId);
+ return UserRolesQueries.getRolesForUser(this, tenantIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1876,7 +1902,7 @@ public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId
private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) throws
StorageQueryException {
try {
- return UserRoleQueries.getRolesForUser(this, appIdentifier, userId);
+ return UserRolesQueries.getRolesForUser(this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1886,7 +1912,7 @@ private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) thr
public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws
StorageQueryException {
try {
- return UserRoleQueries.getUsersForRole(this, tenantIdentifier, role);
+ return UserRolesQueries.getUsersForRole(this, tenantIdentifier, role);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1896,7 +1922,7 @@ public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role)
public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws
StorageQueryException {
try {
- return UserRoleQueries.getPermissionsForRole(this, appIdentifier, role);
+ return UserRolesQueries.getPermissionsForRole(this, appIdentifier, role);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1906,7 +1932,7 @@ public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role)
public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String permission)
throws StorageQueryException {
try {
- return UserRoleQueries.getRolesThatHavePermission(this, appIdentifier, permission);
+ return UserRolesQueries.getRolesThatHavePermission(this, appIdentifier, permission);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1915,7 +1941,7 @@ public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String p
@Override
public boolean deleteRole(AppIdentifier appIdentifier, String role) throws StorageQueryException {
try {
- return UserRoleQueries.deleteRole(this, appIdentifier, role);
+ return UserRolesQueries.deleteRole(this, appIdentifier, role);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1924,7 +1950,7 @@ public boolean deleteRole(AppIdentifier appIdentifier, String role) throws Stora
@Override
public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryException {
try {
- return UserRoleQueries.getRoles(this, appIdentifier);
+ return UserRolesQueries.getRoles(this, appIdentifier);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1933,7 +1959,7 @@ public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryExcepti
@Override
public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws StorageQueryException {
try {
- return UserRoleQueries.doesRoleExist(this, appIdentifier, role);
+ return UserRolesQueries.doesRoleExist(this, appIdentifier, role);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -1943,28 +1969,19 @@ public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws St
public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws
StorageQueryException {
try {
- return UserRoleQueries.deleteAllRolesForUser(this, tenantIdentifier, userId);
+ return UserRolesQueries.deleteAllRolesForUser(this, tenantIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
- public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws
- StorageQueryException {
- try {
- UserRoleQueries.deleteAllRolesForUser(this, appIdentifier, userId);
- } catch (SQLException e) {
- throw new StorageQueryException(e);
- }
- }
-
- @Override
- public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId, String role)
+ public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
+ String userId, String role)
throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return UserRoleQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId,
+ return UserRolesQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId,
role);
} catch (SQLException e) {
throw new StorageQueryException(e);
@@ -1977,7 +1994,7 @@ public boolean createNewRoleOrDoNothingIfExists_Transaction(AppIdentifier appIde
throws StorageQueryException, TenantOrAppNotFoundException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return UserRoleQueries.createNewRoleOrDoNothingIfExists_Transaction(
+ return UserRolesQueries.createNewRoleOrDoNothingIfExists_Transaction(
this, sqlCon, appIdentifier, role);
} catch (SQLException e) {
if (e instanceof SQLiteException) {
@@ -2003,7 +2020,7 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app
throws StorageQueryException, UnknownRoleException {
Connection sqlCon = (Connection) con.getConnection();
try {
- UserRoleQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, appIdentifier,
+ UserRolesQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, appIdentifier,
role, permission);
} catch (SQLException e) {
if (e instanceof SQLiteException) {
@@ -2022,11 +2039,12 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app
}
@Override
- public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role, String permission)
+ public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
+ String role, String permission)
throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return UserRoleQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role,
+ return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role,
permission);
} catch (SQLException e) {
throw new StorageQueryException(e);
@@ -2034,11 +2052,12 @@ public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier,
}
@Override
- public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role)
+ public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
+ String role)
throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return UserRoleQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier,
+ return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier,
role);
} catch (SQLException e) {
throw new StorageQueryException(e);
@@ -2046,10 +2065,22 @@ public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier,
}
@Override
- public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) throws StorageQueryException {
+ public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role)
+ throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
- return UserRoleQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role);
+ return UserRolesQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public void deleteAllRolesForUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ UserRolesQueries.deleteAllRolesForUser_Transaction(sqlCon, this, appIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@@ -2075,7 +2106,8 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU
throw new UnknownSuperTokensUserIdException();
}
- if (isPrimaryKeyError(serverErrorMessage, config.getUserIdMappingTable(), new String[]{"app_id", "supertokens_user_id", "external_user_id"})) {
+ if (isPrimaryKeyError(serverErrorMessage, config.getUserIdMappingTable(),
+ new String[]{"app_id", "supertokens_user_id", "external_user_id"})) {
throw new UserIdMappingAlreadyExistsException(true, true);
}
@@ -2095,7 +2127,8 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU
}
@Override
- public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException {
+ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId)
+ throws StorageQueryException {
try {
if (isSuperTokensUserId) {
return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier,
@@ -2110,7 +2143,8 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, b
}
@Override
- public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException {
+ public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId)
+ throws StorageQueryException {
try {
if (isSuperTokensUserId) {
return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier,
@@ -2173,13 +2207,17 @@ public void createTenant(TenantConfig tenantConfig)
if (e.actualException instanceof SQLiteException) {
String errorMessage = e.actualException.getMessage();
SQLiteConfig config = Config.getConfig(this);
- if (isPrimaryKeyError(errorMessage, config.getTenantConfigsTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id"})) {
+ if (isPrimaryKeyError(errorMessage, config.getTenantConfigsTable(),
+ new String[]{"connection_uri_domain", "app_id", "tenant_id"})) {
throw new DuplicateTenantException();
}
- if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProvidersTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) {
+ if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProvidersTable(),
+ new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) {
throw new DuplicateThirdPartyIdException();
}
- if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProviderClientsTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id", "client_type"})) {
+ if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProviderClientsTable(),
+ new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id",
+ "client_type"})) {
throw new DuplicateClientTypeException();
}
}
@@ -2195,7 +2233,8 @@ public void addTenantIdInTargetStorage(TenantIdentifier tenantIdentifier)
} catch (StorageTransactionLogicException e) {
if (e.actualException instanceof SQLiteException) {
String errorMessage = e.actualException.getMessage();
- if (isPrimaryKeyError(errorMessage, Config.getConfig(this).getTenantsTable(), new String[]{"app_id", "tenant_id"})) {
+ if (isPrimaryKeyError(errorMessage, Config.getConfig(this).getTenantsTable(),
+ new String[]{"app_id", "tenant_id"})) {
throw new DuplicateTenantException();
}
}
@@ -2216,12 +2255,15 @@ public void overwriteTenantConfig(TenantConfig tenantConfig)
if (e.actualException instanceof SQLiteException) {
SQLiteConfig config = Config.getConfig(this);
if (isPrimaryKeyError(e.actualException.getMessage(),
- config.getTenantThirdPartyProvidersTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) {
+ config.getTenantThirdPartyProvidersTable(),
+ new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) {
throw new DuplicateThirdPartyIdException();
}
if (isPrimaryKeyError(e.actualException.getMessage(),
- config.getTenantThirdPartyProviderClientsTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id", "client_type"})) {
+ config.getTenantThirdPartyProviderClientsTable(),
+ new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id",
+ "client_type"})) {
throw new DuplicateClientTypeException();
}
}
@@ -2255,71 +2297,64 @@ public TenantConfig[] getAllTenants() throws StorageQueryException {
}
@Override
- public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId)
- throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException,
- DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException {
+ public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId)
+ throws StorageQueryException, TenantOrAppNotFoundException, DuplicateEmailException,
+ DuplicateThirdPartyUserException, DuplicatePhoneNumberException, UnknownUserIdException {
+ Connection sqlCon = (Connection) con.getConnection();
try {
- return this.startTransaction(con -> {
- Connection sqlCon = (Connection) con.getConnection();
- try {
- String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier,
- userId);
-
- if (recipeId == null) {
- throw new StorageTransactionLogicException(new UnknownUserIdException());
- }
-
- boolean added;
- if (recipeId.equals("emailpassword")) {
- added = EmailPasswordQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
- } else if (recipeId.equals("thirdparty")) {
- added = ThirdPartyQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
- } else if (recipeId.equals("passwordless")) {
- added = PasswordlessQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
- } else {
- throw new IllegalStateException("Should never come here!");
- }
+ String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier,
+ userId);
- sqlCon.commit();
- return added;
- } catch (SQLException throwables) {
- throw new StorageTransactionLogicException(throwables);
- }
- });
- } catch (StorageTransactionLogicException e) {
- if (e.actualException instanceof SQLException) {
- SQLiteConfig config = Config.getConfig(this);
- String serverErrorMessage = e.actualException.getMessage();
+ if (recipeId == null) {
+ throw new UnknownUserIdException();
+ }
- if (isForeignKeyConstraintError(
- serverErrorMessage,
- config.getTenantsTable(),
- new String[]{"app_id", "tenant_id"},
- new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
- throw new TenantOrAppNotFoundException(tenantIdentifier);
- }
- if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) {
- throw new DuplicateEmailException();
- }
- if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(), new String[]{"app_id", "tenant_id", "third_party_id", "third_party_user_id"})) {
- throw new DuplicateThirdPartyUserException();
- }
- if (isUniqueConstraintError(serverErrorMessage,
- Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "phone_number"})) {
- throw new DuplicatePhoneNumberException();
- }
- if (isUniqueConstraintError(serverErrorMessage,
- Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) {
- throw new DuplicateEmailException();
- }
+ boolean added;
+ if (recipeId.equals("emailpassword")) {
+ added = EmailPasswordQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier,
+ userId);
+ } else if (recipeId.equals("thirdparty")) {
+ added = ThirdPartyQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
+ } else if (recipeId.equals("passwordless")) {
+ added = PasswordlessQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier,
+ userId);
+ } else {
+ throw new IllegalStateException("Should never come here!");
+ }
- throw new StorageQueryException(e.actualException);
- } else if (e.actualException instanceof UnknownUserIdException) {
- throw (UnknownUserIdException) e.actualException;
- } else if (e.actualException instanceof StorageQueryException) {
- throw (StorageQueryException) e.actualException;
+ sqlCon.commit();
+ return added;
+ } catch (SQLException throwables) {
+ SQLiteConfig config = Config.getConfig(this);
+ String serverErrorMessage = throwables.getMessage();
+
+ if (isForeignKeyConstraintError(
+ serverErrorMessage,
+ config.getTenantsTable(),
+ new String[]{"app_id", "tenant_id"},
+ new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
+ throw new TenantOrAppNotFoundException(tenantIdentifier);
}
- throw new StorageQueryException(e.actualException);
+ if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "email"})) {
+ throw new DuplicateEmailException();
+ }
+ if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "third_party_id", "third_party_user_id"})) {
+ throw new DuplicateThirdPartyUserException();
+ }
+ if (isUniqueConstraintError(serverErrorMessage,
+ Config.getConfig(this).getPasswordlessUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "phone_number"})) {
+ throw new DuplicatePhoneNumberException();
+ }
+ if (isUniqueConstraintError(serverErrorMessage,
+ Config.getConfig(this).getPasswordlessUserToTenantTable(),
+ new String[]{"app_id", "tenant_id", "email"})) {
+ throw new DuplicateEmailException();
+ }
+
+ throw new StorageQueryException(throwables);
}
}
@@ -2340,11 +2375,14 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String
boolean removed;
if (recipeId.equals("emailpassword")) {
- removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
+ removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon,
+ tenantIdentifier, userId);
} else if (recipeId.equals("thirdparty")) {
- removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
+ removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier,
+ userId);
} else if (recipeId.equals("passwordless")) {
- removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId);
+ removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier,
+ userId);
} else {
throw new IllegalStateException("Should never come here!");
}
@@ -2366,7 +2404,8 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String
}
@Override
- public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException {
+ public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
try {
return DashboardQueries.deleteDashboardUserWithUserId(this, appIdentifier, userId);
} catch (SQLException e) {
@@ -2414,7 +2453,8 @@ public DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentif
}
@Override
- public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) throws StorageQueryException {
+ public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId)
+ throws StorageQueryException {
try {
return DashboardQueries.deleteDashboardUserSessionWithSessionId(this, appIdentifier,
sessionId);
@@ -2424,7 +2464,8 @@ public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String se
}
@Override
- public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId,
+ public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
+ String userId,
String newEmail) throws StorageQueryException,
io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, UserIdNotFoundException {
Connection sqlCon = (Connection) con.getConnection();
@@ -2497,7 +2538,8 @@ public void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser us
SQLiteConfig config = Config.getConfig(this);
String serverErrorMessage = e.getMessage();
- if (isPrimaryKeyError(serverErrorMessage, config.getDashboardUsersTable(), new String[]{"app_id", "user_id"})) {
+ if (isPrimaryKeyError(serverErrorMessage, config.getDashboardUsersTable(),
+ new String[]{"app_id", "user_id"})) {
throw new io.supertokens.pluginInterface.dashboard.exceptions.DuplicateUserIdException();
}
if (isUniqueConstraintError(serverErrorMessage, config.getDashboardUsersTable(),
@@ -2546,7 +2588,8 @@ public void createDevice(AppIdentifier appIdentifier, TOTPDevice device)
if (e.actualException instanceof SQLiteException) {
String errMsg = e.actualException.getMessage();
- if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), new String[]{"app_id", "user_id", "device_name"})) {
+ if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(),
+ new String[]{"app_id", "user_id", "device_name"})) {
throw new DeviceAlreadyExistsException();
} else if (isForeignKeyConstraintError(
errMsg,
@@ -2621,11 +2664,12 @@ public void updateDeviceName(AppIdentifier appIdentifier, String userId, String
} catch (SQLException e) {
if (e instanceof SQLiteException) {
String errMsg = e.getMessage();
- if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), new String[]{"app_id", "user_id", "device_name"})) {
+ if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(),
+ new String[]{"app_id", "user_id", "device_name"})) {
throw new DeviceAlreadyExistsException();
}
}
- throw new StorageQueryException(e);
+ throw new StorageQueryException(e);
}
}
@@ -2659,7 +2703,8 @@ public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifi
try {
TOTPQueries.insertUsedCode_Transaction(this, sqlCon, tenantIdentifier, usedCodeObj);
} catch (SQLException e) {
- if (isPrimaryKeyError(e.getMessage(), Config.getConfig(this).getTotpUsedCodesTable(), new String[]{"app_id", "tenant_id", "user_id", "created_time_ms"})) {
+ if (isPrimaryKeyError(e.getMessage(), Config.getConfig(this).getTotpUsedCodesTable(),
+ new String[]{"app_id", "tenant_id", "user_id", "created_time_ms"})) {
throw new UsedCodeAlreadyExistsException();
} else if (isForeignKeyConstraintError(
@@ -2738,4 +2783,168 @@ public String[] getAllTablesInTheDatabaseThatHasDataForAppId(String appId) throw
throw new StorageQueryException(e);
}
}
+
+ @Override
+ public AuthRecipeUserInfo getPrimaryUserById_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
+ String userId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ return GeneralQueries.getPrimaryUserInfoForUserId_Transaction(this, sqlCon, appIdentifier, userId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(AppIdentifier appIdentifier,
+ TransactionConnection con, String email)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ return GeneralQueries.listPrimaryUsersByEmail_Transaction(this, sqlCon, appIdentifier, email);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(AppIdentifier appIdentifier,
+ TransactionConnection con,
+ String phoneNumber)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ return GeneralQueries.listPrimaryUsersByPhoneNumber_Transaction(this, sqlCon, appIdentifier,
+ phoneNumber);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(AppIdentifier appIdentifier,
+ String thirdPartyId,
+ String thirdPartyUserId)
+ throws StorageQueryException {
+ try {
+ return GeneralQueries.listPrimaryUsersByThirdPartyInfo(this, appIdentifier,
+ thirdPartyId, thirdPartyUserId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(AppIdentifier appIdentifier,
+ TransactionConnection con,
+ String thirdPartyId,
+ String thirdPartyUserId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ return GeneralQueries.listPrimaryUsersByThirdPartyInfo_Transaction(this, sqlCon, appIdentifier,
+ thirdPartyId, thirdPartyUserId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public void makePrimaryUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ // we do not bother returning if a row was updated here or not, cause it's happening
+ // in a transaction anyway.
+ GeneralQueries.makePrimaryUser_Transaction(this, sqlCon, appIdentifier, userId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public void linkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String recipeUserId,
+ String primaryUserId) throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ // we do not bother returning if a row was updated here or not, cause it's happening
+ // in a transaction anyway.
+ GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public void unlinkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String primaryUserId, String recipeUserId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ // we do not bother returning if a row was updated here or not, cause it's happening
+ // in a transaction anyway.
+ GeneralQueries.unlinkAccounts_Transaction(this, sqlCon, appIdentifier, primaryUserId, recipeUserId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public boolean checkIfUsesAccountLinking(AppIdentifier appIdentifier) throws StorageQueryException {
+ try {
+ return GeneralQueries.checkIfUsesAccountLinking(this, appIdentifier);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public int countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException {
+ try {
+ return ActiveUsersQueries.countUsersActiveSinceAndHasMoreThanOneLoginMethod(this, appIdentifier, sinceTime);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public int getUsersCountWithMoreThanOneLoginMethod(AppIdentifier appIdentifier) throws StorageQueryException {
+ try {
+ return GeneralQueries.getUsersCountWithMoreThanOneLoginMethod(this, appIdentifier);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+
+ @Override
+ public UserIdMapping getUserIdMapping_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ if (isSuperTokensUserId) {
+ return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId_Transaction(this, sqlCon, appIdentifier,
+ userId);
+ }
+
+ return UserIdMappingQueries.getUserIdMappingWithExternalUserId_Transaction(this, sqlCon, appIdentifier, userId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
+ @Override
+ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId)
+ throws StorageQueryException {
+ try {
+ Connection sqlCon = (Connection) con.getConnection();
+ return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(this,
+ sqlCon,
+ appIdentifier,
+ userId);
+ } catch (SQLException e) {
+ throw new StorageQueryException(e);
+ }
+ }
+
}
diff --git a/src/main/java/io/supertokens/inmemorydb/Utils.java b/src/main/java/io/supertokens/inmemorydb/Utils.java
new file mode 100644
index 000000000..ce8bfa367
--- /dev/null
+++ b/src/main/java/io/supertokens/inmemorydb/Utils.java
@@ -0,0 +1,30 @@
+/*
+ * 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 io.supertokens.inmemorydb;
+
+public class Utils {
+ public static String generateCommaSeperatedQuestionMarks(int size) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < size; i++) {
+ builder.append("?");
+ if (i != size - 1) {
+ builder.append(",");
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java
index 7ca5d26b1..48aacda21 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java
@@ -1,5 +1,6 @@
package io.supertokens.inmemorydb.queries;
+import java.sql.Connection;
import java.sql.SQLException;
import io.supertokens.inmemorydb.config.Config;
@@ -22,7 +23,8 @@ static String getQueryToCreateUserLastActiveTable(Start start) {
+ " );";
}
- public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException {
+ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getUserLastActiveTable()
+ " WHERE app_id = ? AND last_active_time >= ?";
@@ -37,7 +39,30 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier
});
}
- public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException {
+ public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, long sinceTime)
+ throws SQLException, StorageQueryException {
+ String QUERY = "SELECT count(1) as c FROM ("
+ + " SELECT count(user_id) as num_login_methods, app_id, primary_or_recipe_user_id"
+ + " FROM " + Config.getConfig(start).getUsersTable()
+ + " WHERE primary_or_recipe_user_id IN ("
+ + " SELECT user_id FROM " + Config.getConfig(start).getUserLastActiveTable()
+ + " WHERE app_id = ? AND last_active_time >= ?"
+ + " )"
+ + " GROUP BY app_id, primary_or_recipe_user_id"
+ + ") uc WHERE num_login_methods > 1";
+ return execute(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setLong(2, sinceTime);
+ }, result -> {
+ if (result.next()) {
+ return result.getInt("c");
+ }
+ return 0;
+ });
+ }
+
+ public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable()
+ " WHERE app_id = ?";
@@ -51,8 +76,10 @@ public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier
});
}
- public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException {
- String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() + " AS totp_users "
+ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime)
+ throws SQLException, StorageQueryException {
+ String QUERY =
+ "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() + " AS totp_users "
+ "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active "
+ "ON totp_users.user_id = user_last_active.user_id "
+ "WHERE user_last_active.app_id = ? AND user_last_active.last_active_time >= ?";
@@ -68,9 +95,12 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier
});
}
- public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException {
+ public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable()
- + "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, user_id) DO UPDATE SET last_active_time = ?";
+ +
+ "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, user_id) DO UPDATE SET " +
+ "last_active_time = ?";
long now = System.currentTimeMillis();
return update(start, QUERY, pst -> {
@@ -101,12 +131,13 @@ public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifie
}
}
- public static void deleteUserActive(Start start, AppIdentifier appIdentifier, String userId)
+ public static void deleteUserActive_Transaction(Connection con, Start start, AppIdentifier appIdentifier,
+ String userId)
throws StorageQueryException, SQLException {
String QUERY = "DELETE FROM " + Config.getConfig(start).getUserLastActiveTable()
+ " WHERE app_id = ? AND user_id = ?";
- update(start, QUERY, pst -> {
+ update(con, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, userId);
});
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java
index 587bfd610..425768e39 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java
@@ -37,9 +37,9 @@
public class DashboardQueries {
public static String getQueryToCreateDashboardUsersTable(Start start) {
- String tableName = Config.getConfig(start).getDashboardUsersTable();
+ String dashboardUsersTable = Config.getConfig(start).getDashboardUsersTable();
// @formatter:off
- return "CREATE TABLE IF NOT EXISTS " + tableName + " ("
+ return "CREATE TABLE IF NOT EXISTS " + dashboardUsersTable + " ("
+ "app_id VARCHAR(64) DEFAULT 'public',"
+ "user_id CHAR(36) NOT NULL,"
+ "email VARCHAR(256) NOT NULL,"
@@ -287,5 +287,4 @@ public DashboardSessionInfo[] extract(ResultSet result) throws SQLException, Sto
return temp.toArray(DashboardSessionInfo[]::new);
}
}
-
-}
\ No newline at end of file
+}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java
index 18649b2c7..fb27d870f 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java
@@ -16,13 +16,15 @@
package io.supertokens.inmemorydb.queries;
-import io.supertokens.inmemorydb.ConnectionPool;
import io.supertokens.inmemorydb.ConnectionWithLocks;
import io.supertokens.inmemorydb.Start;
+import io.supertokens.inmemorydb.Utils;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.RowMapper;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
@@ -32,6 +34,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
+import java.util.stream.Collectors;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
@@ -50,7 +53,8 @@ static String getQueryToCreateUsersTable(Start start) {
+ "time_joined BIGINT UNSIGNED NOT NULL,"
+ "PRIMARY KEY (app_id, user_id),"
+ "FOREIGN KEY(app_id, user_id)"
- + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE"
+ + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() +
+ " (app_id, user_id) ON DELETE CASCADE"
+ ");";
}
@@ -65,7 +69,8 @@ static String getQueryToCreateEmailPasswordUserToTenantTable(Start start) {
+ "UNIQUE (app_id, tenant_id, email),"
+ "PRIMARY KEY (app_id, tenant_id, user_id),"
+ "FOREIGN KEY (app_id, tenant_id, user_id)"
- + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE"
+ + " REFERENCES " + Config.getConfig(start).getUsersTable() +
+ "(app_id, tenant_id, user_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
@@ -75,9 +80,10 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) {
+ "app_id VARCHAR(64) DEFAULT 'public',"
+ "user_id CHAR(36) NOT NULL,"
+ "token VARCHAR(128) NOT NULL UNIQUE,"
+ + "email VARCHAR(256)," // nullable cause of backwards compatibility.
+ "token_expiry BIGINT UNSIGNED NOT NULL,"
+ "PRIMARY KEY (app_id, user_id, token),"
- + "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getEmailPasswordUsersTable()
+ + "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable()
+ " (app_id, user_id) ON DELETE CASCADE ON UPDATE CASCADE"
+ ");";
}
@@ -93,7 +99,8 @@ public static void deleteExpiredPasswordResetTokens(Start start) throws SQLExcep
update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis()));
}
- public static void updateUsersPassword_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newPassword)
+ public static void updateUsersPassword_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String userId, String newPassword)
throws SQLException, StorageQueryException {
String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable()
+ " SET password_hash = ? WHERE app_id = ? AND user_id = ?";
@@ -105,7 +112,8 @@ public static void updateUsersPassword_Transaction(Start start, Connection con,
});
}
- public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newEmail)
+ public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String userId, String newEmail)
throws SQLException, StorageQueryException {
{
String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable()
@@ -129,10 +137,12 @@ public static void updateUsersEmail_Transaction(Start start, Connection con, App
}
}
- public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId)
+ public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier, String userId)
throws SQLException, StorageQueryException {
- String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?";
+ String QUERY =
+ "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?";
update(con, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
@@ -143,7 +153,8 @@ public static void deleteAllPasswordResetTokensForUser_Transaction(Start start,
public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(Start start, AppIdentifier appIdentifier,
String userId)
throws StorageQueryException, SQLException {
- String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable()
+ String QUERY =
+ "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable()
+ " WHERE app_id = ? AND user_id = ?";
return execute(start, QUERY, pst -> {
@@ -167,11 +178,13 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans
String userId)
throws SQLException, StorageQueryException {
- ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getPasswordResetTokensTable());
+ ((ConnectionWithLocks) con).lock(
+ appIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getPasswordResetTokensTable());
- String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable()
- + " WHERE app_id = ? AND user_id = ?";
+ String QUERY =
+ "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable()
+ + " WHERE app_id = ? AND user_id = ?";
return execute(con, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
@@ -189,30 +202,11 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans
});
}
- public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
- String id)
- throws SQLException, StorageQueryException {
-
- ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + id + Config.getConfig(start).getEmailPasswordUsersTable());
-
- String QUERY = "SELECT user_id, email, password_hash, time_joined FROM "
- + getConfig(start).getEmailPasswordUsersTable()
- + " WHERE app_id = ? AND user_id = ?";
- UserInfoPartial userInfo = execute(con, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, id);
- }, result -> {
- if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
- }
- return null;
- });
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfo);
- }
-
- public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, String token)
+ public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier,
+ String token)
throws SQLException, StorageQueryException {
- String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable()
+ String QUERY =
+ "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable()
+ " WHERE app_id = ? AND token = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
@@ -225,43 +219,62 @@ public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppI
});
}
- public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, long expiry)
+ public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash,
+ long expiry, String email)
throws SQLException, StorageQueryException {
- String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable()
- + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)";
+ if (email != null) {
+ String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable()
+ + "(app_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?)";
- update(start, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
- pst.setString(3, tokenHash);
- pst.setLong(4, expiry);
- });
+ update(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ pst.setString(3, tokenHash);
+ pst.setLong(4, expiry);
+ pst.setString(5, email);
+ });
+ } else {
+ String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable()
+ + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)";
+
+ update(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ pst.setString(3, tokenHash);
+ pst.setLong(4, expiry);
+ });
+ }
}
- public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, String passwordHash, long timeJoined)
+ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email,
+ String passwordHash, long timeJoined)
throws StorageQueryException, StorageTransactionLogicException {
return start.startTransaction(con -> {
Connection sqlCon = (Connection) con.getConnection();
try {
{ // app_id_to_user_id
String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable()
- + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)";
+ + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, userId);
- pst.setString(3, EMAIL_PASSWORD.toString());
+ pst.setString(3, userId);
+ pst.setString(4, EMAIL_PASSWORD.toString());
});
}
{ // all_auth_recipe_users
String QUERY = "INSERT INTO " + getConfig(start).getUsersTable()
- + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)";
+ + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" +
+ " VALUES(?, ?, ?, ?, ?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userId);
- pst.setString(4, EMAIL_PASSWORD.toString());
- pst.setLong(5, timeJoined);
+ pst.setString(4, userId);
+ pst.setString(5, EMAIL_PASSWORD.toString());
+ pst.setLong(6, timeJoined);
+ pst.setLong(7, timeJoined);
});
}
@@ -290,57 +303,62 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St
});
}
- UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(userId, email, passwordHash, timeJoined));
-
+ UserInfoPartial userInfo = new UserInfoPartial(userId, email, passwordHash, timeJoined);
+ fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo);
+ fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo);
sqlCon.commit();
- return userInfo;
+ return AuthRecipeUserInfo.create(userId, false, userInfo.toLoginMethod());
} catch (SQLException throwables) {
throw new StorageTransactionLogicException(throwables);
}
});
}
- public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId)
- throws StorageQueryException, StorageTransactionLogicException {
- start.startTransaction(con -> {
- Connection sqlCon = (Connection) con.getConnection();
- try {
- {
- String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable()
- + " WHERE app_id = ? AND user_id = ?";
-
- update(sqlCon, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
- });
- }
+ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier,
+ String userId, boolean deleteUserIdMappingToo)
+ throws StorageQueryException, SQLException {
+ if (deleteUserIdMappingToo) {
+ String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable()
+ + " WHERE app_id = ? AND user_id = ?";
- sqlCon.commit();
- } catch (SQLException throwables) {
- throw new StorageTransactionLogicException(throwables);
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ } else {
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
}
- return null;
- });
- }
- public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException {
- String QUERY = "SELECT user_id, email, password_hash, time_joined FROM "
- + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?";
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getEmailPasswordUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
- UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, id);
- }, result -> {
- if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
}
- return null;
- });
- return userInfoWithTenantIds(start, appIdentifier, userInfo);
+ }
}
- public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException {
- // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id table
+ private static UserInfoPartial getUserInfoUsingId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
+ String id) throws SQLException, StorageQueryException {
+ // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on
+ // app_id_to_user_id table
String QUERY = "SELECT user_id, email, password_hash, time_joined FROM "
+ getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?";
@@ -355,29 +373,55 @@ public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon,
});
}
- public static List getUsersInfoUsingIdList(Start start, AppIdentifier appIdentifier, List ids)
+ public static List getUsersInfoUsingIdList(Start start, Set ids,
+ AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
if (ids.size() > 0) {
// No need to filter based on tenantId because the id list is already filtered for a tenant
- StringBuilder QUERY = new StringBuilder("SELECT user_id, email, password_hash, time_joined "
- + "FROM " + getConfig(start).getEmailPasswordUsersTable());
- QUERY.append(" WHERE app_id = ? AND user_id IN (");
- for (int i = 0; i < ids.size(); i++) {
-
- QUERY.append("?");
- if (i != ids.size() - 1) {
- // not the last element
- QUERY.append(",");
+ String QUERY = "SELECT user_id, email, password_hash, time_joined "
+ + "FROM " + getConfig(start).getEmailPasswordUsersTable()
+ + " WHERE user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) +
+ " ) AND app_id = ?";
+
+ List userInfos = execute(start, QUERY, pst -> {
+ int index = 1;
+ for (String id : ids) {
+ pst.setString(index, id);
+ index++;
}
- }
- QUERY.append(")");
+ pst.setString(index, appIdentifier.getAppId());
+ }, result -> {
+ List finalResult = new ArrayList<>();
+ while (result.next()) {
+ finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result));
+ }
+ return finalResult;
+ });
+ fillUserInfoWithTenantIds(start, appIdentifier, userInfos);
+ fillUserInfoWithVerified(start, appIdentifier, userInfos);
+ return userInfos.stream().map(UserInfoPartial::toLoginMethod)
+ .collect(Collectors.toList());
+ }
+ return Collections.emptyList();
+ }
- List userInfos = execute(start, QUERY.toString(), pst -> {
- pst.setString(1, appIdentifier.getAppId());
- for (int i = 0; i < ids.size(); i++) {
- // i+2 cause this starts with 1 and not 0, and 1 is appId
- pst.setString(i + 2, ids.get(i));
+ public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids,
+ AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
+ if (ids.size() > 0) {
+ // No need to filter based on tenantId because the id list is already filtered for a tenant
+ String QUERY = "SELECT user_id, email, password_hash, time_joined "
+ + "FROM " + getConfig(start).getEmailPasswordUsersTable()
+ + " WHERE user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) +
+ " ) AND app_id = ?";
+
+ List userInfos = execute(con, QUERY, pst -> {
+ int index = 1;
+ for (String id : ids) {
+ pst.setString(index, id);
+ index++;
}
+ pst.setString(index, appIdentifier.getAppId());
}, result -> {
List finalResult = new ArrayList<>();
while (result.next()) {
@@ -385,55 +429,106 @@ public static List getUsersInfoUsingIdList(Start start, AppIdentifier
}
return finalResult;
});
- return userInfoWithTenantIds(start, appIdentifier, userInfos);
+ fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos);
+ return userInfos.stream().map(UserInfoPartial::toLoginMethod)
+ .collect(Collectors.toList());
}
return Collections.emptyList();
}
+ public static String lockEmail_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier,
+ String email)
+ throws StorageQueryException, SQLException {
+ String QUERY = "SELECT user_id FROM " + getConfig(start).getEmailPasswordUsersTable() +
+ " WHERE app_id = ? AND email = ?";
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, email);
+ }, result -> {
+ if (result.next()) {
+ return result.getString("user_id");
+ }
+ return null;
+ });
+ }
- public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException {
- String QUERY = "SELECT ep_users_to_tenant.user_id as user_id, ep_users_to_tenant.email as email, "
- + "ep_users.password_hash as password_hash, ep_users.time_joined as time_joined "
- + "FROM " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep_users_to_tenant "
- + "JOIN " + getConfig(start).getEmailPasswordUsersTable() + " AS ep_users "
- + "ON ep_users.app_id = ep_users_to_tenant.app_id AND ep_users.user_id = ep_users_to_tenant.user_id "
- + "WHERE ep_users_to_tenant.app_id = ? AND ep_users_to_tenant.tenant_id = ? AND ep_users_to_tenant.email = ?";
+ public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier,
+ String email)
+ throws StorageQueryException, SQLException {
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" +
+ " WHERE ep.app_id = ? AND ep.tenant_id = ? AND ep.email = ?";
- UserInfoPartial userInfo = execute(start, QUERY, pst -> {
+ return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, email);
}, result -> {
if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ return result.getString("user_id");
}
return null;
});
- return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo);
+ }
+
+ public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String email)
+ throws StorageQueryException, SQLException {
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep" +
+ " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" +
+ " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" +
+ " WHERE ep.app_id = ? AND ep.email = ?";
+
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, email);
+ }, result -> {
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
+ }
+ return userIds;
+ });
}
public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon,
TenantIdentifier tenantIdentifier, String userId)
- throws SQLException, StorageQueryException {
- UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId(start, sqlCon,
+ throws SQLException, StorageQueryException, UnknownUserIdException {
+ UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId_Transaction(start, sqlCon,
tenantIdentifier.toAppIdentifier(), userId);
+ if (userInfo == null) {
+ throw new UnknownUserIdException();
+ }
+
+ GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId);
+
{ // all_auth_recipe_users
String QUERY = "INSERT INTO " + getConfig(start).getUsersTable()
- + "(app_id, tenant_id, user_id, recipe_id, time_joined)"
- + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
+ + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)"
+ + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
+ GeneralQueries.AccountLinkingInfo finalAccountLinkingInfo = accountLinkingInfo;
+
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userId);
- pst.setString(4, EMAIL_PASSWORD.toString());
- pst.setLong(5, userInfo.timeJoined);
+ pst.setString(4, finalAccountLinkingInfo.primaryUserId);
+ pst.setBoolean(5, finalAccountLinkingInfo.isLinked);
+ pst.setString(6, EMAIL_PASSWORD.toString());
+ pst.setLong(7, userInfo.timeJoined);
+ pst.setLong(8, userInfo.timeJoined);
});
}
{ // emailpassword_user_to_tenant
String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable()
+ "(app_id, tenant_id, user_id, email)"
- + " VALUES(?, ?, ?, ?) " + " ON CONFLICT DO NOTHING";
+ + " VALUES(?, ?, ?, ?) " + " ON CONFLICT (app_id, tenant_id, user_id) DO NOTHING";
int numRows = update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
@@ -446,7 +541,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC
}
}
- public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId)
+ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon,
+ TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
{ // all_auth_recipe_users
String QUERY = "DELETE FROM " + getConfig(start).getUsersTable()
@@ -462,42 +558,103 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection
// automatically deleted from emailpassword_user_to_tenant because of foreign key constraint
}
- private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo)
+ private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ UserInfoPartial userInfo)
throws SQLException, StorageQueryException {
if (userInfo == null) return null;
- try (Connection con = ConnectionPool.getConnection(start)) {
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0);
- }
+ return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0);
}
- private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos)
+ private static List fillUserInfoWithVerified_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userInfos)
throws SQLException, StorageQueryException {
- try (Connection con = ConnectionPool.getConnection(start)) {
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ List userIdsAndEmails = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email));
}
+ List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon,
+ appIdentifier,
+ userIdsAndEmails);
+ Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified);
+ for (UserInfoPartial userInfo : userInfos) {
+ if (verifiedUserIdsSet.contains(userInfo.id)) {
+ userInfo.verified = true;
+ } else {
+ userInfo.verified = false;
+ }
+ }
+ return userInfos;
}
- private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo)
+ private static List fillUserInfoWithVerified(Start start,
+ AppIdentifier appIdentifier,
+ List userInfos)
+ throws SQLException, StorageQueryException {
+ List userIdsAndEmails = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email));
+ }
+ List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified(start,
+ appIdentifier,
+ userIdsAndEmails);
+ Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified);
+ for (UserInfoPartial userInfo : userInfos) {
+ if (verifiedUserIdsSet.contains(userInfo.id)) {
+ userInfo.verified = true;
+ } else {
+ userInfo.verified = false;
+ }
+ }
+ return userInfos;
+ }
+
+ private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ UserInfoPartial userInfo)
throws SQLException, StorageQueryException {
if (userInfo == null) return null;
- return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0);
+ return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0);
+ }
+
+ private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userInfos)
+ throws SQLException, StorageQueryException {
+ String[] userIds = new String[userInfos.size()];
+ for (int i = 0; i < userInfos.size(); i++) {
+ userIds[i] = userInfos.get(i).id;
+ }
+
+ Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon,
+ appIdentifier,
+ userIds);
+ List result = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]);
+ }
+
+ return userInfos;
}
- private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos)
+ private static List fillUserInfoWithTenantIds(Start start,
+ AppIdentifier appIdentifier,
+ List userInfos)
throws SQLException, StorageQueryException {
String[] userIds = new String[userInfos.size()];
for (int i = 0; i < userInfos.size(); i++) {
userIds[i] = userInfos.get(i).id;
}
- Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, appIdentifier, userIds);
- List result = new ArrayList<>();
+ Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds(start,
+ appIdentifier,
+ userIds);
for (UserInfoPartial userInfo : userInfos) {
- result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined,
- tenantIdsForUserIds.get(userInfo.id).toArray(new String[0])));
+ userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]);
}
- return result;
+ return userInfos;
}
private static class UserInfoPartial {
@@ -505,6 +662,9 @@ private static class UserInfoPartial {
public final long timeJoined;
public final String email;
public final String passwordHash;
+ public String[] tenantIds;
+ public Boolean verified;
+ public Boolean isPrimary;
public UserInfoPartial(String id, String email, String passwordHash, long timeJoined) {
this.id = id.trim();
@@ -512,6 +672,13 @@ public UserInfoPartial(String id, String email, String passwordHash, long timeJo
this.email = email;
this.passwordHash = passwordHash;
}
+
+ public LoginMethod toLoginMethod() {
+ assert (tenantIds != null);
+ assert (verified != null);
+ return new LoginMethod(id, timeJoined, verified, email,
+ passwordHash, tenantIds);
+ }
}
private static class PasswordResetRowMapper implements RowMapper {
@@ -528,7 +695,7 @@ private static PasswordResetRowMapper getInstance() {
public PasswordResetTokenInfo map(ResultSet result) throws StorageQueryException {
try {
return new PasswordResetTokenInfo(result.getString("user_id"), result.getString("token"),
- result.getLong("token_expiry"));
+ result.getLong("token_expiry"), result.getString("email"));
} catch (Exception e) {
throw new StorageQueryException(e);
}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java
index 849c8e9fa..c571427ba 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java
@@ -17,8 +17,8 @@
package io.supertokens.inmemorydb.queries;
import io.supertokens.inmemorydb.ConnectionWithLocks;
-import io.supertokens.inmemorydb.QueryExecutorTemplate;
import io.supertokens.inmemorydb.Start;
+import io.supertokens.inmemorydb.Utils;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.RowMapper;
import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo;
@@ -28,11 +28,9 @@
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import java.sql.Connection;
-import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
@@ -80,7 +78,8 @@ public static void deleteExpiredEmailVerificationTokens(Start start) throws SQLE
public static void updateUsersIsEmailVerified_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
String userId, String email,
- boolean isEmailVerified) throws SQLException, StorageQueryException {
+ boolean isEmailVerified)
+ throws SQLException, StorageQueryException {
if (isEmailVerified) {
String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTable()
@@ -104,8 +103,10 @@ public static void updateUsersIsEmailVerified_Transaction(Start start, Connectio
}
public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con,
- TenantIdentifier tenantIdentifier, String userId,
- String email) throws SQLException, StorageQueryException {
+ TenantIdentifier tenantIdentifier,
+ String userId,
+ String email)
+ throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable()
+ " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?";
@@ -117,7 +118,8 @@ public static void deleteAllEmailVerificationTokensForUser_Transaction(Start sta
});
}
- public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, TenantIdentifier tenantIdentifier,
+ public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start,
+ TenantIdentifier tenantIdentifier,
String token)
throws SQLException, StorageQueryException {
String QUERY = "SELECT user_id, token, token_expiry, email FROM "
@@ -135,7 +137,8 @@ public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start sta
});
}
- public static void addEmailVerificationToken(Start start, TenantIdentifier tenantIdentifier, String userId, String tokenHash, long expiry,
+ public static void addEmailVerificationToken(Start start, TenantIdentifier tenantIdentifier, String userId,
+ String tokenHash, long expiry,
String email) throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTokensTable()
+ "(app_id, tenant_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?, ?)";
@@ -153,12 +156,17 @@ public static void addEmailVerificationToken(Start start, TenantIdentifier tenan
public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(Start start,
Connection con,
TenantIdentifier tenantIdentifier,
- String userId, String email) throws SQLException, StorageQueryException {
+ String userId,
+ String email)
+ throws SQLException, StorageQueryException {
- ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + userId + "~" + email + Config.getConfig(start).getEmailVerificationTokensTable());
+ ((ConnectionWithLocks) con).lock(
+ tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + userId + "~" + email +
+ Config.getConfig(start).getEmailVerificationTokensTable());
String QUERY = "SELECT user_id, token, token_expiry, email FROM "
- + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?";
+ + getConfig(start).getEmailVerificationTokensTable() +
+ " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?";
return execute(con, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
@@ -181,9 +189,11 @@ public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUs
public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Start start,
TenantIdentifier tenantIdentifier,
String userId,
- String email) throws SQLException, StorageQueryException {
+ String email)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT user_id, token, token_expiry, email FROM "
- + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?";
+ + getConfig(start).getEmailVerificationTokensTable() +
+ " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
@@ -215,38 +225,130 @@ public static boolean isEmailVerified(Start start, AppIdentifier appIdentifier,
}, result -> result.next());
}
- public static void deleteUserInfo(Start start, AppIdentifier appIdentifier, String userId)
- throws StorageQueryException, StorageTransactionLogicException {
- start.startTransaction(con -> {
- Connection sqlCon = (Connection) con.getConnection();
- try {
- {
- String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable()
- + " WHERE app_id = ? AND user_id = ?";
- update(sqlCon, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
- });
- }
+ public static class UserIdAndEmail {
+ public String userId;
+ public String email;
- {
- String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable()
- + " WHERE app_id = ? AND user_id = ?";
+ public UserIdAndEmail(String userId, String email) {
+ this.userId = userId;
+ this.email = email;
+ }
+ }
+
+ // returns list of userIds where email is verified.
+ public static List isEmailVerified_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
+ List userIdAndEmail)
+ throws SQLException, StorageQueryException {
+ if (userIdAndEmail.isEmpty()) {
+ return new ArrayList<>();
+ }
+ List emails = new ArrayList<>();
+ List userIds = new ArrayList<>();
+ Map userIdToEmailMap = new HashMap<>();
+ for (UserIdAndEmail ue : userIdAndEmail) {
+ emails.add(ue.email);
+ userIds.add(ue.userId);
+ }
+ for (UserIdAndEmail ue : userIdAndEmail) {
+ if (userIdToEmailMap.containsKey(ue.userId)) {
+ throw new RuntimeException("Found a bug!");
+ }
+ userIdToEmailMap.put(ue.userId, ue.email);
+ }
+ String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable()
+ + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")";
- update(sqlCon, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
- });
+ return execute(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ int index = 2;
+ for (String userId : userIds) {
+ pst.setString(index++, userId);
+ }
+ for (String email : emails) {
+ pst.setString(index++, email);
+ }
+ }, result -> {
+ List res = new ArrayList<>();
+ while (result.next()) {
+ String userId = result.getString("user_id");
+ String email = result.getString("email");
+ if (Objects.equals(userIdToEmailMap.get(userId), email)) {
+ res.add(userId);
}
+ }
+ return res;
+ });
+ }
- sqlCon.commit();
- } catch (SQLException throwables) {
- throw new StorageTransactionLogicException(throwables);
+ public static List isEmailVerified(Start start, AppIdentifier appIdentifier,
+ List userIdAndEmail)
+ throws SQLException, StorageQueryException {
+ if (userIdAndEmail.isEmpty()) {
+ return new ArrayList<>();
+ }
+ List emails = new ArrayList<>();
+ List userIds = new ArrayList<>();
+ Map userIdToEmailMap = new HashMap<>();
+ for (UserIdAndEmail ue : userIdAndEmail) {
+ emails.add(ue.email);
+ userIds.add(ue.userId);
+ }
+ for (UserIdAndEmail ue : userIdAndEmail) {
+ if (userIdToEmailMap.containsKey(ue.userId)) {
+ throw new RuntimeException("Found a bug!");
}
- return null;
+ userIdToEmailMap.put(ue.userId, ue.email);
+ }
+ String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable()
+ + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")";
+
+ return execute(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ int index = 2;
+ for (String userId : userIds) {
+ pst.setString(index++, userId);
+ }
+ for (String email : emails) {
+ pst.setString(index++, email);
+ }
+ }, result -> {
+ List res = new ArrayList<>();
+ while (result.next()) {
+ String userId = result.getString("user_id");
+ String email = result.getString("email");
+ if (Objects.equals(userIdToEmailMap.get(userId), email)) {
+ res.add(userId);
+ }
+ }
+ return res;
});
}
+ public static void deleteUserInfo_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier,
+ String userId)
+ throws StorageQueryException, SQLException {
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable()
+ + " WHERE app_id = ? AND user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+ }
+
public static boolean deleteUserInfo(Start start, TenantIdentifier tenantIdentifier, String userId)
throws StorageQueryException, SQLException {
String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable()
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java
index dc56ccce4..96ccc6773 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java
@@ -20,15 +20,16 @@
import io.supertokens.inmemorydb.ConnectionPool;
import io.supertokens.inmemorydb.ConnectionWithLocks;
import io.supertokens.inmemorydb.Start;
+import io.supertokens.inmemorydb.Utils;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.KeyValueInfo;
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.RowMapper;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
-import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -38,10 +39,8 @@
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
import static io.supertokens.ProcessState.PROCESS_STATE.CREATING_NEW_TABLE;
import static io.supertokens.ProcessState.getInstance;
@@ -74,19 +73,69 @@ static String getQueryToCreateUsersTable(Start start) {
+ "app_id VARCHAR(64) DEFAULT 'public',"
+ "tenant_id VARCHAR(64) DEFAULT 'public',"
+ "user_id CHAR(36) NOT NULL,"
+ + "primary_or_recipe_user_id CHAR(36) NOT NULL,"
+ + "is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE,"
+ + "primary_or_recipe_user_time_joined BIGINT UNSIGNED NOT NULL,"
+ "recipe_id VARCHAR(128) NOT NULL,"
+ "time_joined BIGINT UNSIGNED NOT NULL,"
+ "PRIMARY KEY (app_id, tenant_id, user_id),"
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + Config.getConfig(start).getTenantsTable()
+ " (app_id, tenant_id) ON DELETE CASCADE,"
+ + "FOREIGN KEY (app_id, primary_or_recipe_user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable()
+ + " (app_id, user_id) ON DELETE CASCADE,"
+ "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable()
+ " (app_id, user_id) ON DELETE CASCADE"
+ ");";
}
- static String getQueryToCreateUserPaginationIndex(Start start) {
- return "CREATE INDEX all_auth_recipe_users_pagination_index ON " + Config.getConfig(start).getUsersTable()
- + "(time_joined DESC, user_id DESC, tenant_id DESC, app_id DESC);";
+ public static String getQueryToCreateUserIdIndexForUsersTable(Start start) {
+ return "CREATE INDEX IF NOT EXISTS all_auth_recipe_user_id_index ON "
+ + Config.getConfig(start).getUsersTable() + "(app_id, user_id);";
+ }
+
+ public static String getQueryToCreateTenantIdIndexForUsersTable(Start start) {
+ return "CREATE INDEX IF NOT EXISTS all_auth_recipe_tenant_id_index ON "
+ + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id);";
+ }
+
+ static String getQueryToCreateUserPaginationIndex1(Start start) {
+ return "CREATE INDEX all_auth_recipe_users_pagination_index1 ON " + Config.getConfig(start).getUsersTable()
+ + "(app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);";
+ }
+
+ static String getQueryToCreateUserPaginationIndex2(Start start) {
+ return "CREATE INDEX all_auth_recipe_users_pagination_index2 ON " + Config.getConfig(start).getUsersTable()
+ + "(app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);";
+ }
+
+ static String getQueryToCreateUserPaginationIndex3(Start start) {
+ return "CREATE INDEX all_auth_recipe_users_pagination_index3 ON " + Config.getConfig(start).getUsersTable()
+ + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);";
+ }
+
+ static String getQueryToCreateUserPaginationIndex4(Start start) {
+ return "CREATE INDEX all_auth_recipe_users_pagination_index4 ON " + Config.getConfig(start).getUsersTable()
+ + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);";
+ }
+
+ static String getQueryToCreatePrimaryUserId(Start start) {
+ /*
+ * Used in:
+ * - does user exist
+ *
+ * */
+ return "CREATE INDEX all_auth_recipe_users_primary_user_id_index ON " + Config.getConfig(start).getUsersTable()
+ + "(app_id, primary_or_recipe_user_id);";
+ }
+
+ static String getQueryToCreateRecipeIdIndex(Start start) {
+ /*
+ * Used in:
+ * - user count query
+ * */
+ return "CREATE INDEX all_auth_recipe_users_recipe_id_index ON " +
+ Config.getConfig(start).getUsersTable()
+ + "(app_id, recipe_id, tenant_id);";
}
private static String getQueryToCreateAppsTable(Start start) {
@@ -128,15 +177,23 @@ static String getQueryToCreateKeyValueTable(Start start) {
}
+ static String getQueryToCreateTenantIdIndexForKeyValueTable(Start start) {
+ return "CREATE INDEX IF NOT EXISTS key_value_tenant_id_index ON "
+ + Config.getConfig(start).getKeyValueTable() + "(app_id, tenant_id);";
+ }
- private static String getQueryToCreateAppIdToUserIdTable(Start start) {
+ private static String getQueryToCreateAppIdToUserIdTable(Start start) {
String appToUserTable = Config.getConfig(start).getAppIdToUserIdTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + appToUserTable + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "user_id CHAR(36) NOT NULL,"
+ "recipe_id VARCHAR(128) NOT NULL,"
+ + "primary_or_recipe_user_id CHAR(36) NOT NULL,"
+ + "is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE,"
+ "PRIMARY KEY (app_id, user_id), "
+ + "FOREIGN KEY (app_id, primary_or_recipe_user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable()
+ + " (app_id, user_id) ON DELETE CASCADE,"
+ "FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable()
+ " (app_id) ON DELETE CASCADE"
+ ");";
@@ -157,6 +214,8 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
if (!doesTableExists(start, Config.getConfig(start).getKeyValueTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, getQueryToCreateKeyValueTable(start), NO_OP_SETTER);
+ // index
+ update(start, getQueryToCreateTenantIdIndexForKeyValueTable(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) {
@@ -169,7 +228,12 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
update(start, getQueryToCreateUsersTable(start), NO_OP_SETTER);
// index
- update(start, getQueryToCreateUserPaginationIndex(start), NO_OP_SETTER);
+ update(start, getQueryToCreateUserPaginationIndex1(start), NO_OP_SETTER);
+ update(start, getQueryToCreateUserPaginationIndex2(start), NO_OP_SETTER);
+ update(start, getQueryToCreateUserPaginationIndex3(start), NO_OP_SETTER);
+ update(start, getQueryToCreateUserPaginationIndex4(start), NO_OP_SETTER);
+ update(start, getQueryToCreatePrimaryUserId(start), NO_OP_SETTER);
+ update(start, getQueryToCreateRecipeIdIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getUserLastActiveTable())) {
@@ -199,6 +263,7 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start),
NO_OP_SETTER);
+
}
if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) {
@@ -280,7 +345,6 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
// index
update(start, getQueryToCreateCodeCreatedAtIndex(start), NO_OP_SETTER);
update(start, getQueryToCreateCodeDeviceIdHashIndex(start), NO_OP_SETTER);
-
}
if (!doesTableExists(start, Config.getConfig(start).getUserMetadataTable())) {
@@ -290,22 +354,22 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
if (!doesTableExists(start, Config.getConfig(start).getRolesTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
- update(start, UserRoleQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER);
+ update(start, UserRolesQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getUserRolesPermissionsTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
- update(start, UserRoleQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER);
+ update(start, UserRolesQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER);
// index
- update(start, UserRoleQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER);
+ update(start, UserRolesQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getUserRolesTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
- update(start, UserRoleQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER);
+ update(start, UserRolesQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER);
// index
- update(start, UserRoleQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER);
+ update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) {
@@ -392,7 +456,9 @@ public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con,
String key)
throws SQLException, StorageQueryException {
- ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + key + Config.getConfig(start).getKeyValueTable());
+ ((ConnectionWithLocks) con).lock(
+ tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + key +
+ Config.getConfig(start).getKeyValueTable());
String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable()
+ " WHERE app_id = ? AND tenant_id = ? AND name = ?";
@@ -424,7 +490,9 @@ public static void deleteKeyValue_Transaction(Start start, Connection con, Tenan
public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds)
throws SQLException, StorageQueryException {
- StringBuilder QUERY = new StringBuilder("SELECT COUNT(*) as total FROM " + getConfig(start).getUsersTable());
+ StringBuilder QUERY = new StringBuilder(
+ "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " +
+ getConfig(start).getUsersTable());
QUERY.append(" WHERE app_id = ?");
if (includeRecipeIds != null && includeRecipeIds.length > 0) {
QUERY.append(" AND recipe_id IN (");
@@ -456,7 +524,8 @@ public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIP
public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds)
throws SQLException, StorageQueryException {
- StringBuilder QUERY = new StringBuilder("SELECT COUNT(*) as total FROM " + getConfig(start).getUsersTable());
+ StringBuilder QUERY = new StringBuilder(
+ "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " + getConfig(start).getUsersTable());
QUERY.append(" WHERE app_id = ? AND tenant_id = ?");
if (includeRecipeIds != null && includeRecipeIds.length > 0) {
QUERY.append(" AND recipe_id IN (");
@@ -489,37 +558,46 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier,
public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId)
throws SQLException, StorageQueryException {
-
+ // We query both tables cause there is a case where a primary user ID exists, but its associated
+ // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already.
String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable()
- + " WHERE app_id = ? AND user_id = ?";
+ + " WHERE app_id = ? AND user_id = ? UNION SELECT 1 FROM " + getConfig(start).getUsersTable() +
+ " WHERE app_id = ? AND primary_or_recipe_user_id = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, userId);
+ pst.setString(3, appIdentifier.getAppId());
+ pst.setString(4, userId);
}, ResultSet::next);
}
public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
-
+ // We query both tables cause there is a case where a primary user ID exists, but its associated
+ // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already.
String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable()
- + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?";
+ + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? UNION SELECT 1 FROM " +
+ getConfig(start).getUsersTable() +
+ " WHERE app_id = ? AND tenant_id = ? AND primary_or_recipe_user_id = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userId);
+ pst.setString(4, tenantIdentifier.getAppId());
+ pst.setString(5, tenantIdentifier.getTenantId());
+ pst.setString(6, userId);
}, ResultSet::next);
}
- public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit,
- @NotNull String timeJoinedOrder,
- @org.jetbrains.annotations.Nullable RECIPE_ID[] includeRecipeIds, @org.jetbrains.annotations.Nullable
- String userId,
- @org.jetbrains.annotations.Nullable Long timeJoined,
- @Nullable DashboardSearchTags dashboardSearchTags)
+ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit,
+ @NotNull String timeJoinedOrder,
+ @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId,
+ @Nullable Long timeJoined,
+ @Nullable DashboardSearchTags dashboardSearchTags)
throws SQLException, StorageQueryException {
// This list will be used to keep track of the result's order from the db
- List usersFromQuery;
+ List usersFromQuery;
if (dashboardSearchTags != null) {
ArrayList queryList = new ArrayList<>();
@@ -561,7 +639,9 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant
String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable()
+ " AS allAuthUsersTable"
+ " JOIN " + getConfig(start).getThirdPartyUserToTenantTable()
- + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable.app_id AND"
+ +
+ " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable" +
+ ".app_id AND"
+ " allAuthUsersTable.tenant_id = thirdPartyToTenantTable.tenant_id AND"
+ " allAuthUsersTable.user_id = thirdPartyToTenantTable.user_id"
+ " JOIN " + getConfig(start).getThirdPartyUsersTable()
@@ -691,22 +771,20 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant
usersFromQuery = new ArrayList<>();
} else {
- String finalQuery = "SELECT * FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )"
- + " AS finalResultTable ORDER BY time_joined " + timeJoinedOrder + ", user_id DESC ";
+ String finalQuery = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )"
+ + " AS finalResultTable ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + ", primary_or_recipe_user_id DESC ";
usersFromQuery = execute(start, finalQuery, pst -> {
for (int i = 1; i <= queryList.size(); i++) {
pst.setString(i, queryList.get(i - 1));
}
}, result -> {
- List temp = new ArrayList<>();
+ List temp = new ArrayList<>();
while (result.next()) {
- temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"),
- result.getString("recipe_id")));
+ temp.add(result.getString("primary_or_recipe_user_id"));
}
return temp;
});
}
-
}
} else {
@@ -730,11 +808,11 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant
recipeIdCondition = recipeIdCondition + " AND";
}
String timeJoinedOrderSymbol = timeJoinedOrder.equals("ASC") ? ">" : "<";
- String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE "
- + recipeIdCondition + " (time_joined " + timeJoinedOrderSymbol
- + " ? OR (time_joined = ? AND user_id <= ?)) AND app_id = ? AND tenant_id = ?"
- + " ORDER BY time_joined " + timeJoinedOrder
- + ", user_id DESC LIMIT ?";
+ String QUERY = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM " + getConfig(start).getUsersTable() + " WHERE "
+ + recipeIdCondition + " (primary_or_recipe_user_time_joined " + timeJoinedOrderSymbol
+ + " ? OR (primary_or_recipe_user_time_joined = ? AND primary_or_recipe_user_id <= ?)) AND app_id = ? AND tenant_id = ?"
+ + " ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder
+ + ", primary_or_recipe_user_id DESC LIMIT ?";
usersFromQuery = execute(start, QUERY, pst -> {
if (includeRecipeIds != null) {
for (int i = 0; i < includeRecipeIds.length; i++) {
@@ -750,21 +828,20 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant
pst.setString(baseIndex + 5, tenantIdentifier.getTenantId());
pst.setInt(baseIndex + 6, limit);
}, result -> {
- List temp = new ArrayList<>();
+ List temp = new ArrayList<>();
while (result.next()) {
- temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"),
- result.getString("recipe_id")));
+ temp.add(result.getString("primary_or_recipe_user_id"));
}
return temp;
});
} else {
String recipeIdCondition = RECIPE_ID_CONDITION.toString();
- String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE ";
+ String QUERY = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM " + getConfig(start).getUsersTable() + " WHERE ";
if (!recipeIdCondition.equals("")) {
QUERY += recipeIdCondition + " AND";
}
- QUERY += " app_id = ? AND tenant_id = ? ORDER BY time_joined " + timeJoinedOrder
- + ", user_id DESC LIMIT ?";
+ QUERY += " app_id = ? AND tenant_id = ? ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder
+ + ", primary_or_recipe_user_id DESC LIMIT ?";
usersFromQuery = execute(start, QUERY, pst -> {
if (includeRecipeIds != null) {
for (int i = 0; i < includeRecipeIds.length; i++) {
@@ -777,78 +854,528 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant
pst.setString(baseIndex + 2, tenantIdentifier.getTenantId());
pst.setInt(baseIndex + 3, limit);
}, result -> {
- List temp = new ArrayList<>();
+ List temp = new ArrayList<>();
while (result.next()) {
- temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"),
- result.getString("recipe_id")));
+ temp.add(result.getString("primary_or_recipe_user_id"));
}
return temp;
});
}
}
- // we create a map from recipe ID -> userId[]
- Map> recipeIdToUserIdListMap = new HashMap<>();
- for (UserInfoPaginationResultHolder user : usersFromQuery) {
- RECIPE_ID recipeId = RECIPE_ID.getEnumFromString(user.recipeId);
- if (recipeId == null) {
- throw new SQLException("Unrecognised recipe ID in database: " + user.recipeId);
+ AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()];
+
+ List users = getPrimaryUserInfoForUserIds(start,
+ tenantIdentifier.toAppIdentifier(),
+ usersFromQuery);
+
+ // we fill in all the slots in finalResult based on their position in
+ // usersFromQuery
+ Map userIdToInfoMap = new HashMap<>();
+ for (AuthRecipeUserInfo user : users) {
+ userIdToInfoMap.put(user.getSupertokensUserId(), user);
+ }
+ for (int i = 0; i < usersFromQuery.size(); i++) {
+ if (finalResult[i] == null) {
+ finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i));
}
- List userIdList = recipeIdToUserIdListMap.get(recipeId);
- if (userIdList == null) {
- userIdList = new ArrayList<>();
+ }
+
+ return finalResult;
+ }
+
+ public static void makePrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
+ String userId)
+ throws SQLException, StorageQueryException {
+ {
+ String QUERY = "UPDATE " + getConfig(start).getUsersTable() +
+ " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+ {
+ String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() +
+ " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+ }
+
+ public static void linkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
+ String recipeUserId, String primaryUserId)
+ throws SQLException, StorageQueryException {
+ {
+ String QUERY = "UPDATE " + getConfig(start).getUsersTable() +
+ " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " +
+ "user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, primaryUserId);
+ pst.setString(2, appIdentifier.getAppId());
+ pst.setString(3, recipeUserId);
+ });
+ }
+ { // update primary_or_recipe_user_time_joined to min time joined
+ String QUERY = "UPDATE " + getConfig(start).getUsersTable() +
+ " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " +
+ getConfig(start).getUsersTable() + " WHERE app_id = ? AND primary_or_recipe_user_id = ?) WHERE " +
+ " app_id = ? AND primary_or_recipe_user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, primaryUserId);
+ pst.setString(3, appIdentifier.getAppId());
+ pst.setString(4, primaryUserId);
+ });
+ }
+ {
+ String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() +
+ " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " +
+ "user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, primaryUserId);
+ pst.setString(2, appIdentifier.getAppId());
+ pst.setString(3, recipeUserId);
+ });
+ }
+ }
+
+ public static void unlinkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
+ String primaryUserId, String recipeUserId)
+ throws SQLException, StorageQueryException {
+ {
+ String QUERY = "UPDATE " + getConfig(start).getUsersTable() +
+ " SET is_linked_or_is_a_primary_user = false, primary_or_recipe_user_id = ?, " +
+ "primary_or_recipe_user_time_joined = time_joined WHERE app_id = ? AND " +
+ "user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, recipeUserId);
+ pst.setString(2, appIdentifier.getAppId());
+ pst.setString(3, recipeUserId);
+ });
+ }
+ { // update primary_or_recipe_user_time_joined to min time joined
+ String QUERY = "UPDATE " + getConfig(start).getUsersTable() +
+ " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " +
+ getConfig(start).getUsersTable() + " WHERE app_id = ? AND primary_or_recipe_user_id = ?) WHERE " +
+ " app_id = ? AND primary_or_recipe_user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, primaryUserId);
+ pst.setString(3, appIdentifier.getAppId());
+ pst.setString(4, primaryUserId);
+ });
+ }
+ {
+ String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() +
+ " SET is_linked_or_is_a_primary_user = false, primary_or_recipe_user_id = ?" +
+ " WHERE app_id = ? AND user_id = ?";
+
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, recipeUserId);
+ pst.setString(2, appIdentifier.getAppId());
+ pst.setString(3, recipeUserId);
+ });
+ }
+ }
+
+ public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ String phoneNumber)
+ throws SQLException, StorageQueryException {
+ // we first lock on the table based on phoneNumber and tenant - this will ensure that any other
+ // query happening related to the account linking on this phone number / tenant will wait for this to finish,
+ // and vice versa.
+
+ PasswordlessQueries.lockPhone_Transaction(start, sqlCon, appIdentifier, phoneNumber);
+
+ // now that we have locks on all the relevant tables, we can read from them safely
+ List userIds = PasswordlessQueries.listUserIdsByPhoneNumber_Transaction(start, sqlCon, appIdentifier,
+ phoneNumber);
+
+ List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier,
+ userIds);
+
+ // this is going to order them based on oldest that joined to newest that joined.
+ result.sort(Comparator.comparingLong(o -> o.timeJoined));
+
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(Start start,
+ AppIdentifier appIdentifier,
+ String thirdPartyId,
+ String thirdPartyUserId)
+ throws SQLException, StorageQueryException {
+ List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo(start, appIdentifier,
+ thirdPartyId, thirdPartyUserId);
+ List result = getPrimaryUserInfoForUserIds(start, appIdentifier, userIds);
+
+ // this is going to order them based on oldest that joined to newest that joined.
+ result.sort(Comparator.comparingLong(o -> o.timeJoined));
+
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ String thirdPartyId,
+ String thirdPartyUserId)
+ throws SQLException, StorageQueryException {
+ // we first lock on the table based on thirdparty info and tenant - this will ensure that any other
+ // query happening related to the account linking on this third party info / tenant will wait for this to
+ // finish,
+ // and vice versa.
+
+ ThirdPartyQueries.lockThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, thirdPartyId,
+ thirdPartyUserId);
+
+ // now that we have locks on all the relevant tables, we can read from them safely
+ List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo_Transaction(start, sqlCon, appIdentifier,
+ thirdPartyId, thirdPartyUserId);
+ List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, userIds);
+
+ // this is going to order them based on oldest that joined to newest that joined.
+ result.sort(Comparator.comparingLong(o -> o.timeJoined));
+
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ String email)
+ throws SQLException, StorageQueryException {
+ // we first lock on the three tables based on email and tenant - this will ensure that any other
+ // query happening related to the account linking on this email / tenant will wait for this to finish,
+ // and vice versa.
+
+ EmailPasswordQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email);
+
+ ThirdPartyQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email);
+
+ PasswordlessQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email);
+
+ // now that we have locks on all the relevant tables, we can read from them safely
+ List userIds = new ArrayList<>();
+ userIds.addAll(EmailPasswordQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier,
+ email));
+
+ userIds.addAll(PasswordlessQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier,
+ email));
+
+ userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email));
+
+ // remove duplicates from userIds
+ Set userIdsSet = new HashSet<>(userIds);
+ userIds = new ArrayList<>(userIdsSet);
+
+ List result = getPrimaryUserInfoForUserIds(start, appIdentifier,
+ userIds);
+
+ // this is going to order them based on oldest that joined to newest that joined.
+ result.sort(Comparator.comparingLong(o -> o.timeJoined));
+
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantIdentifier tenantIdentifier,
+ String email)
+ throws StorageQueryException, SQLException {
+ List userIds = new ArrayList<>();
+ String emailPasswordUserId = EmailPasswordQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier,
+ email);
+ if (emailPasswordUserId != null) {
+ userIds.add(emailPasswordUserId);
+ }
+
+ String passwordlessUserId = PasswordlessQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier,
+ email);
+ if (passwordlessUserId != null) {
+ userIds.add(passwordlessUserId);
+ }
+
+ userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email));
+
+ // remove duplicates from userIds
+ Set userIdsSet = new HashSet<>(userIds);
+ userIds = new ArrayList<>(userIdsSet);
+
+ List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(),
+ userIds);
+
+ // this is going to order them based on oldest that joined to newest that joined.
+ result.sort(Comparator.comparingLong(o -> o.timeJoined));
+
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(Start start,
+ TenantIdentifier tenantIdentifier,
+ String phoneNumber)
+ throws StorageQueryException, SQLException {
+ List userIds = new ArrayList<>();
+
+ String passwordlessUserId = PasswordlessQueries.getPrimaryUserByPhoneNumber(start, tenantIdentifier,
+ phoneNumber);
+ if (passwordlessUserId != null) {
+ userIds.add(passwordlessUserId);
+ }
+
+ List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(),
+ userIds);
+
+ // this is going to order them based on oldest that joined to newest that joined.
+ result.sort(Comparator.comparingLong(o -> o.timeJoined));
+
+ return result.toArray(new AuthRecipeUserInfo[0]);
+ }
+
+ public static AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(Start start,
+ TenantIdentifier tenantIdentifier,
+ String thirdPartyId,
+ String thirdPartyUserId)
+ throws StorageQueryException, SQLException {
+ String userId = ThirdPartyQueries.getUserIdByThirdPartyInfo(start, tenantIdentifier,
+ thirdPartyId, thirdPartyUserId);
+ return getPrimaryUserInfoForUserId(start, tenantIdentifier.toAppIdentifier(), userId);
+ }
+
+ public static String getPrimaryUserIdStrForUserId(Start start, AppIdentifier appIdentifier, String id)
+ throws SQLException, StorageQueryException {
+ String QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() +
+ " WHERE user_id = ? AND app_id = ?";
+ return execute(start, QUERY, pst -> {
+ pst.setString(1, id);
+ pst.setString(2, appIdentifier.getAppId());
+ }, result -> {
+ if (result.next()) {
+ return result.getString("primary_or_recipe_user_id");
+ }
+ return null;
+ });
+ }
+
+ public static AuthRecipeUserInfo getPrimaryUserInfoForUserId(Start start, AppIdentifier appIdentifier, String id)
+ throws SQLException, StorageQueryException {
+ List ids = new ArrayList<>();
+ ids.add(id);
+ List result = getPrimaryUserInfoForUserIds(start, appIdentifier, ids);
+ if (result.isEmpty()) {
+ return null;
+ }
+ return result.get(0);
+ }
+
+ public static AuthRecipeUserInfo getPrimaryUserInfoForUserId_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier, String id)
+ throws SQLException, StorageQueryException {
+ List ids = new ArrayList<>();
+ ids.add(id);
+ List result = getPrimaryUserInfoForUserIds_Transaction(start, con, appIdentifier, ids);
+ if (result.isEmpty()) {
+ return null;
+ }
+ return result.get(0);
+ }
+
+ private static List getPrimaryUserInfoForUserIds(Start start,
+ AppIdentifier appIdentifier,
+ List userIds)
+ throws StorageQueryException, SQLException {
+ if (userIds.size() == 0) {
+ return new ArrayList<>();
+ }
+
+ // We check both user_id and primary_or_recipe_user_id because the input may have a recipe userId
+ // which is linked to a primary user ID in which case it won't be in the primary_or_recipe_user_id column,
+ // or the input may have a primary user ID whose recipe user ID was removed, so it won't be in the user_id
+ // column
+ String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, aaru.tenant_id, aaru.time_joined FROM " + getConfig(start).getAppIdToUserIdTable() + " as au " +
+ "LEFT JOIN " + getConfig(start).getUsersTable() + " as aaru ON au.app_id = aaru.app_id AND au.user_id = aaru.user_id" +
+ " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " +
+ getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN ("
+ + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ ") OR au.primary_or_recipe_user_id IN (" +
+ Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ ")) AND app_id = ?) AND au.app_id = ?";
+
+ List allAuthUsersResult = execute(start, QUERY, pst -> {
+ // IN user_id
+ int index = 1;
+ for (int i = 0; i < userIds.size(); i++, index++) {
+ pst.setString(index, userIds.get(i));
+ }
+ // IN primary_or_recipe_user_id
+ for (int i = 0; i < userIds.size(); i++, index++) {
+ pst.setString(index, userIds.get(i));
+ }
+ // for app_id
+ pst.setString(index, appIdentifier.getAppId());
+ pst.setString(index + 1, appIdentifier.getAppId());
+ }, result -> {
+ List parsedResult = new ArrayList<>();
+ while (result.next()) {
+ parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"),
+ result.getString("tenant_id"),
+ result.getString("primary_or_recipe_user_id"),
+ result.getBoolean("is_linked_or_is_a_primary_user"),
+ result.getString("recipe_id"),
+ result.getLong("time_joined")));
}
- userIdList.add(user.userId);
- recipeIdToUserIdListMap.put(recipeId, userIdList);
+ return parsedResult;
+ });
+
+ // Now we form the userIds again, but based on the user_id in the result from above.
+ Set recipeUserIdsToFetch = new HashSet<>();
+ for (AllAuthRecipeUsersResultHolder user : allAuthUsersResult) {
+ // this will remove duplicate entries wherein a user id is shared across several tenants.
+ recipeUserIdsToFetch.add(user.userId);
}
- AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()];
+ List loginMethods = new ArrayList<>();
+ loginMethods.addAll(
+ EmailPasswordQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
+ loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
+ loginMethods.addAll(
+ PasswordlessQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
+
+ Map recipeUserIdToLoginMethodMap = new HashMap<>();
+ for (LoginMethod loginMethod : loginMethods) {
+ recipeUserIdToLoginMethodMap.put(loginMethod.getSupertokensUserId(), loginMethod);
+ }
- // we give the userId[] for each recipe to fetch all those user's details
- for (RECIPE_ID recipeId : recipeIdToUserIdListMap.keySet()) {
- List extends AuthRecipeUserInfo> users = getUserInfoForRecipeIdFromUserIds(start,
- tenantIdentifier.toAppIdentifier(), recipeId, recipeIdToUserIdListMap.get(recipeId));
+ Map userIdToAuthRecipeUserInfo = new HashMap<>();
+
+ for (AllAuthRecipeUsersResultHolder authRecipeUsersResultHolder : allAuthUsersResult) {
+ String recipeUserId = authRecipeUsersResultHolder.userId;
+ LoginMethod loginMethod = recipeUserIdToLoginMethodMap.get(recipeUserId);
- // we fill in all the slots in finalResult based on their position in
- // usersFromQuery
- Map userIdToInfoMap = new HashMap<>();
- for (AuthRecipeUserInfo user : users) {
- userIdToInfoMap.put(user.id, user);
+ if (loginMethod == null) {
+ // loginMethod will be null for primaryUserId for which the user has been deleted during unlink
+ continue;
}
- for (int i = 0; i < usersFromQuery.size(); i++) {
- if (finalResult[i] == null) {
- finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i).userId);
- }
+
+ String primaryUserId = authRecipeUsersResultHolder.primaryOrRecipeUserId;
+ AuthRecipeUserInfo curr = userIdToAuthRecipeUserInfo.get(primaryUserId);
+ if (curr == null) {
+ curr = AuthRecipeUserInfo.create(primaryUserId, authRecipeUsersResultHolder.isLinkedOrIsAPrimaryUser,
+ loginMethod);
+ } else {
+ curr.addLoginMethod(loginMethod);
}
+ userIdToAuthRecipeUserInfo.put(primaryUserId, curr);
}
- return finalResult;
+ return userIdToAuthRecipeUserInfo.keySet().stream().map(userIdToAuthRecipeUserInfo::get)
+ .collect(Collectors.toList());
}
- private static List extends AuthRecipeUserInfo> getUserInfoForRecipeIdFromUserIds(Start start,
- AppIdentifier appIdentifier,
- RECIPE_ID recipeId,
- List userIds)
+ private static List getPrimaryUserInfoForUserIds_Transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userIds)
throws StorageQueryException, SQLException {
- if (recipeId == RECIPE_ID.EMAIL_PASSWORD) {
- return EmailPasswordQueries.getUsersInfoUsingIdList(start, appIdentifier, userIds);
- } else if (recipeId == RECIPE_ID.THIRD_PARTY) {
- return ThirdPartyQueries.getUsersInfoUsingIdList(start, appIdentifier, userIds);
- } else if (recipeId == RECIPE_ID.PASSWORDLESS) {
- return PasswordlessQueries.getUsersByIdList(start, appIdentifier, userIds);
- } else {
- throw new IllegalArgumentException("No implementation of get users for recipe: " + recipeId.toString());
+ if (userIds.size() == 0) {
+ return new ArrayList<>();
+ }
+
+ // We check both user_id and primary_or_recipe_user_id because the input may have a recipe userId
+ // which is linked to a primary user ID in which case it won't be in the primary_or_recipe_user_id column,
+ // or the input may have a primary user ID whose recipe user ID was removed, so it won't be in the user_id
+ // column
+ String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, aaru.tenant_id, aaru.time_joined FROM " + getConfig(start).getAppIdToUserIdTable() + " as au" +
+ " LEFT JOIN " + getConfig(start).getUsersTable() + " as aaru ON au.app_id = aaru.app_id AND au.user_id = aaru.user_id" +
+ " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " +
+ getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN ("
+ + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ ") OR au.primary_or_recipe_user_id IN (" +
+ Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ ")) AND app_id = ?) AND au.app_id = ?";
+
+ List allAuthUsersResult = execute(sqlCon, QUERY, pst -> {
+ // IN user_id
+ int index = 1;
+ for (int i = 0; i < userIds.size(); i++, index++) {
+ pst.setString(index, userIds.get(i));
+ }
+ // IN primary_or_recipe_user_id
+ for (int i = 0; i < userIds.size(); i++, index++) {
+ pst.setString(index, userIds.get(i));
+ }
+ // for app_id
+ pst.setString(index, appIdentifier.getAppId());
+ pst.setString(index + 1, appIdentifier.getAppId());
+ }, result -> {
+ List parsedResult = new ArrayList<>();
+ while (result.next()) {
+ parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"),
+ result.getString("tenant_id"),
+ result.getString("primary_or_recipe_user_id"),
+ result.getBoolean("is_linked_or_is_a_primary_user"),
+ result.getString("recipe_id"),
+ result.getLong("time_joined")));
+ }
+ return parsedResult;
+ });
+
+ // Now we form the userIds again, but based on the user_id in the result from above.
+ Set recipeUserIdsToFetch = new HashSet<>();
+ for (AllAuthRecipeUsersResultHolder user : allAuthUsersResult) {
+ // this will remove duplicate entries wherein a user id is shared across several tenants.
+ recipeUserIdsToFetch.add(user.userId);
+ }
+
+ List loginMethods = new ArrayList<>();
+ loginMethods.addAll(
+ EmailPasswordQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier));
+ loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier));
+ loginMethods.addAll(
+ PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier));
+
+ Map recipeUserIdToLoginMethodMap = new HashMap<>();
+ for (LoginMethod loginMethod : loginMethods) {
+ recipeUserIdToLoginMethodMap.put(loginMethod.getSupertokensUserId(), loginMethod);
+ }
+
+ Map userIdToAuthRecipeUserInfo = new HashMap<>();
+
+ for (AllAuthRecipeUsersResultHolder authRecipeUsersResultHolder : allAuthUsersResult) {
+ String recipeUserId = authRecipeUsersResultHolder.userId;
+ LoginMethod loginMethod = recipeUserIdToLoginMethodMap.get(recipeUserId);
+ if (loginMethod == null) {
+ // loginMethod will be null for primaryUserId for which the user has been deleted during unlink
+ continue;
+ }
+ String primaryUserId = authRecipeUsersResultHolder.primaryOrRecipeUserId;
+ AuthRecipeUserInfo curr = userIdToAuthRecipeUserInfo.get(primaryUserId);
+ if (curr == null) {
+ curr = AuthRecipeUserInfo.create(primaryUserId, authRecipeUsersResultHolder.isLinkedOrIsAPrimaryUser,
+ loginMethod);
+ } else {
+ curr.addLoginMethod(loginMethod);
+ }
+ userIdToAuthRecipeUserInfo.put(primaryUserId, curr);
}
+
+ return userIdToAuthRecipeUserInfo.keySet().stream().map(userIdToAuthRecipeUserInfo::get)
+ .collect(Collectors.toList());
}
- public static String getRecipeIdForUser_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId)
+ public static String getRecipeIdForUser_Transaction(Start start, Connection sqlCon,
+ TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
- ((ConnectionWithLocks) sqlCon).lock(tenantIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getAppIdToUserIdTable());
+ ((ConnectionWithLocks) sqlCon).lock(
+ tenantIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getAppIdToUserIdTable());
String QUERY = "SELECT recipe_id FROM " + getConfig(start).getAppIdToUserIdTable()
+ " WHERE app_id = ? AND user_id = ?";
-
return execute(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, userId);
@@ -860,12 +1387,14 @@ public static String getRecipeIdForUser_Transaction(Start start, Connection sqlC
});
}
- public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String[] userIds)
+ public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ String[] userIds)
throws SQLException, StorageQueryException {
if (userIds != null && userIds.length > 0) {
StringBuilder QUERY = new StringBuilder("SELECT user_id, tenant_id "
+ "FROM " + getConfig(start).getUsersTable());
- QUERY.append(" WHERE app_id = ? AND user_id IN (");
+ QUERY.append(" WHERE user_id IN (");
for (int i = 0; i < userIds.length; i++) {
QUERY.append("?");
@@ -874,14 +1403,57 @@ public static Map> getTenantIdsForUserIds_transaction(Start
QUERY.append(",");
}
}
- QUERY.append(")");
+ QUERY.append(") AND app_id = ?");
return execute(sqlCon, QUERY.toString(), pst -> {
- pst.setString(1, appIdentifier.getAppId());
for (int i = 0; i < userIds.length; i++) {
- // i+2 cause this starts with 1 and not 0, and 1 is appId
- pst.setString(i + 2, userIds[i]);
+ // i+1 cause this starts with 1 and not 0, and 1 is appId
+ pst.setString(i + 1, userIds[i]);
+ }
+ pst.setString(userIds.length + 1, appIdentifier.getAppId());
+ }, result -> {
+ Map> finalResult = new HashMap<>();
+ for (String userId : userIds) {
+ finalResult.put(userId, new ArrayList<>());
+ }
+
+ while (result.next()) {
+ String userId = result.getString("user_id").trim();
+ String tenantId = result.getString("tenant_id");
+
+ finalResult.get(userId).add(tenantId);
+ }
+ return finalResult;
+ });
+ }
+
+ return new HashMap<>();
+ }
+
+ public static Map> getTenantIdsForUserIds(Start start,
+ AppIdentifier appIdentifier,
+ String[] userIds)
+ throws SQLException, StorageQueryException {
+ if (userIds != null && userIds.length > 0) {
+ StringBuilder QUERY = new StringBuilder("SELECT user_id, tenant_id "
+ + "FROM " + getConfig(start).getUsersTable());
+ QUERY.append(" WHERE user_id IN (");
+ for (int i = 0; i < userIds.length; i++) {
+
+ QUERY.append("?");
+ if (i != userIds.length - 1) {
+ // not the last element
+ QUERY.append(",");
+ }
+ }
+ QUERY.append(") AND app_id = ?");
+
+ return execute(start, QUERY.toString(), pst -> {
+ for (int i = 0; i < userIds.length; i++) {
+ // i+1 cause this starts with 1 and not 0, and 1 is appId
+ pst.setString(i + 1, userIds[i]);
}
+ pst.setString(userIds.length + 1, appIdentifier.getAppId());
}, result -> {
Map> finalResult = new HashMap<>();
for (String userId : userIds) {
@@ -909,7 +1481,7 @@ public static String[] getAllTablesInTheDatabase(Start start) throws SQLExceptio
List tableNames = new ArrayList<>();
try (Connection con = ConnectionPool.getConnection(start)) {
DatabaseMetaData metadata = con.getMetaData();
- ResultSet resultSet = metadata.getTables(null, null, null, new String[] { "TABLE" });
+ ResultSet resultSet = metadata.getTables(null, null, null, new String[]{"TABLE"});
while (resultSet.next()) {
String tableName = resultSet.getString("TABLE_NAME");
tableNames.add(tableName);
@@ -944,13 +1516,89 @@ public static String[] getAllTablesInTheDatabaseThatHasDataForAppId(Start start,
return result.toArray(new String[0]);
}
- private static class UserInfoPaginationResultHolder {
- String userId;
- String recipeId;
+ public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
+ String QUERY = "SELECT COUNT (1) as c FROM ("
+ + " SELECT COUNT(user_id) as num_login_methods "
+ + " FROM " + getConfig(start).getUsersTable()
+ + " WHERE app_id = ? "
+ + " GROUP BY (app_id, primary_or_recipe_user_id) "
+ + ") as nloginmethods WHERE num_login_methods > 1";
+
+ return execute(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ }, result -> {
+ return result.next() ? result.getInt("c") : 0;
+ });
+ }
+
+ public static boolean checkIfUsesAccountLinking(Start start, AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
+ String QUERY = "SELECT 1 FROM "
+ + getConfig(start).getUsersTable()
+ + " WHERE app_id = ? AND is_linked_or_is_a_primary_user = true LIMIT 1";
+
+ return execute(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ }, result -> {
+ return result.next();
+ });
+ }
+
+ public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
+ GeneralQueries.AccountLinkingInfo accountLinkingInfo = new GeneralQueries.AccountLinkingInfo(userId, false);
+ {
+ String QUERY = "SELECT primary_or_recipe_user_id, is_linked_or_is_a_primary_user FROM "
+ + Config.getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?";
+
+ accountLinkingInfo = execute(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ }, result -> {
+ if (result.next()) {
+ String primaryUserId1 = result.getString("primary_or_recipe_user_id");
+ boolean isLinked1 = result.getBoolean("is_linked_or_is_a_primary_user");
+ return new AccountLinkingInfo(primaryUserId1, isLinked1);
+
+ }
+ return null;
+ });
+ }
+ return accountLinkingInfo;
+ }
+
+ public static boolean doesUserIdExist_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
+ // We query both tables cause there is a case where a primary user ID exists, but its associated
+ // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already.
+ String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable()
+ + " WHERE app_id = ? AND user_id = ? UNION SELECT 1 FROM " + getConfig(start).getUsersTable() +
+ " WHERE app_id = ? AND primary_or_recipe_user_id = ?";
+ return execute(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ pst.setString(3, appIdentifier.getAppId());
+ pst.setString(4, userId);
+ }, ResultSet::next);
+ }
- UserInfoPaginationResultHolder(String userId, String recipeId) {
- this.userId = userId;
- this.recipeId = recipeId;
+ private static class AllAuthRecipeUsersResultHolder {
+ String userId;
+ String tenantId;
+ String primaryOrRecipeUserId;
+ boolean isLinkedOrIsAPrimaryUser;
+ RECIPE_ID recipeId;
+ long timeJoined;
+
+ AllAuthRecipeUsersResultHolder(String userId, String tenantId, String primaryOrRecipeUserId,
+ boolean isLinkedOrIsAPrimaryUser, String recipeId, long timeJoined) {
+ this.userId = userId.trim();
+ this.tenantId = tenantId;
+ this.primaryOrRecipeUserId = primaryOrRecipeUserId;
+ this.isLinkedOrIsAPrimaryUser = isLinkedOrIsAPrimaryUser;
+ this.recipeId = RECIPE_ID.getEnumFromString(recipeId);
+ this.timeJoined = timeJoined;
}
}
@@ -969,4 +1617,14 @@ public KeyValueInfo map(ResultSet result) throws Exception {
return new KeyValueInfo(result.getString("value"), result.getLong("created_at_time"));
}
}
+
+ public static class AccountLinkingInfo {
+ public String primaryUserId;
+ public boolean isLinked;
+
+ public AccountLinkingInfo(String primaryUserId, boolean isLinked) {
+ this.primaryUserId = primaryUserId;
+ this.isLinked = isLinked;
+ }
+ }
}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java
index da667a45b..82af7379d 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java
@@ -216,7 +216,7 @@ public static void addTenantIdInTargetStorage(Start start, TenantIdentifier tena
{
String QUERY = "INSERT INTO " + Config.getConfig(start).getTenantsTable()
- + "(app_id, tenant_id, created_at_time)" + " VALUES(?, ?, ?) ON CONFLICT DO NOTHING";
+ + "(app_id, tenant_id, created_at_time)" + " VALUES(?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java
index 7694189d8..54e5a49f3 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java
@@ -19,15 +19,18 @@
import io.supertokens.inmemorydb.ConnectionPool;
import io.supertokens.inmemorydb.ConnectionWithLocks;
import io.supertokens.inmemorydb.Start;
+import io.supertokens.inmemorydb.Utils;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.RowMapper;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
-import io.supertokens.pluginInterface.passwordless.UserInfo;
import io.supertokens.pluginInterface.sqlStorage.SQLStorage.TransactionIsolationLevel;
import javax.annotation.Nonnull;
@@ -36,6 +39,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
+import java.util.stream.Collectors;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
@@ -69,7 +73,8 @@ static String getQueryToCreatePasswordlessUserToTenantTable(Start start) {
+ "UNIQUE (app_id, tenant_id, phone_number),"
+ "PRIMARY KEY (app_id, tenant_id, user_id),"
+ "FOREIGN KEY (app_id, tenant_id, user_id)"
- + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE"
+ + " REFERENCES " + Config.getConfig(start).getUsersTable() +
+ "(app_id, tenant_id, user_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
@@ -85,7 +90,7 @@ public static String getQueryToCreateDevicesTable(Start start) {
+ "failed_attempts INT UNSIGNED NOT NULL,"
+ "PRIMARY KEY (app_id, tenant_id, device_id_hash),"
+ "FOREIGN KEY(app_id, tenant_id)"
- + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE"
+ + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
}
@@ -98,8 +103,9 @@ public static String getQueryToCreateCodesTable(Start start) {
+ "link_code_hash CHAR(44) NOT NULL,"
+ "created_at BIGINT UNSIGNED NOT NULL,"
+ "PRIMARY KEY (app_id, tenant_id, code_id),"
- + "UNIQUE (app_id, tenant_id, link_code_hash),"
- + "FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES " + Config.getConfig(start).getPasswordlessDevicesTable()
+ + "UNIQUE (app_id, tenant_id, link_code_hash),"
+ + "FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES " +
+ Config.getConfig(start).getPasswordlessDevicesTable()
+ " (app_id, tenant_id, device_id_hash) ON DELETE CASCADE ON UPDATE CASCADE"
+ ");";
}
@@ -111,7 +117,8 @@ public static String getQueryToCreateDeviceEmailIndex(Start start) {
public static String getQueryToCreateDevicePhoneNumberIndex(Start start) {
return "CREATE INDEX passwordless_devices_phone_number_index ON "
- + Config.getConfig(start).getPasswordlessDevicesTable() + " (app_id, tenant_id, phone_number);"; // USING hash
+ + Config.getConfig(start).getPasswordlessDevicesTable() +
+ " (app_id, tenant_id, phone_number);"; // USING hash
}
public static String getQueryToCreateCodeDeviceIdHashIndex(Start start) {
@@ -125,8 +132,10 @@ public static String getQueryToCreateCodeCreatedAtIndex(Start start) {
}
- public static void createDeviceWithCode(Start start, TenantIdentifier tenantIdentifier, String email, String phoneNumber, String linkCodeSalt,
- PasswordlessCode code) throws StorageTransactionLogicException, StorageQueryException {
+ public static void createDeviceWithCode(Start start, TenantIdentifier tenantIdentifier, String email,
+ String phoneNumber, String linkCodeSalt,
+ PasswordlessCode code)
+ throws StorageTransactionLogicException, StorageQueryException {
start.startTransaction(con -> {
Connection sqlCon = (Connection) con.getConnection();
try {
@@ -155,7 +164,9 @@ public static PasswordlessDevice getDevice_Transaction(Start start, Connection c
TenantIdentifier tenantIdentifier, String deviceIdHash)
throws StorageQueryException, SQLException {
- ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + deviceIdHash + Config.getConfig(start).getPasswordlessDevicesTable());
+ ((ConnectionWithLocks) con).lock(
+ tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + deviceIdHash +
+ Config.getConfig(start).getPasswordlessDevicesTable());
String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM "
+ getConfig(start).getPasswordlessDevicesTable()
@@ -173,7 +184,8 @@ public static PasswordlessDevice getDevice_Transaction(Start start, Connection c
}
public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Connection con,
- TenantIdentifier tenantIdentifier, String deviceIdHash)
+ TenantIdentifier tenantIdentifier,
+ String deviceIdHash)
throws SQLException, StorageQueryException {
String QUERY = "UPDATE " + getConfig(start).getPasswordlessDevicesTable()
+ " SET failed_attempts = failed_attempts + 1"
@@ -186,7 +198,8 @@ public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Co
});
}
- public static void deleteDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String deviceIdHash)
+ public static void deleteDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
+ String deviceIdHash)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable()
+ " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?";
@@ -197,7 +210,9 @@ public static void deleteDevice_Transaction(Start start, Connection con, TenantI
});
}
- public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber)
+ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con,
+ TenantIdentifier tenantIdentifier,
+ @Nonnull String phoneNumber)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable()
@@ -210,7 +225,8 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio
});
}
- public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String phoneNumber, String userId)
+ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ @Nonnull String phoneNumber, String userId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable()
@@ -227,7 +243,8 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio
});
}
- public static void deleteDevicesByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String email)
+ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
+ @Nonnull String email)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable()
@@ -240,7 +257,8 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con,
});
}
- public static void deleteDevicesByEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String email, String userId)
+ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ @Nonnull String email, String userId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable()
@@ -257,7 +275,8 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con,
});
}
- private static void createCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, PasswordlessCode code)
+ private static void createCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
+ PasswordlessCode code)
throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessCodesTable()
+ "(app_id, tenant_id, code_id, device_id_hash, link_code_hash, created_at)"
@@ -287,7 +306,9 @@ public static void createCode(Start start, TenantIdentifier tenantIdentifier, Pa
});
}
- public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String deviceIdHash)
+ public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con,
+ TenantIdentifier tenantIdentifier,
+ String deviceIdHash)
throws StorageQueryException, SQLException {
// We do not lock here, since the device is already locked earlier in the transaction.
String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM "
@@ -311,7 +332,9 @@ public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Conne
});
}
- public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String linkCodeHash)
+ public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con,
+ TenantIdentifier tenantIdentifier,
+ String linkCodeHash)
throws StorageQueryException, SQLException {
// We do not lock here, since the device is already locked earlier in the transaction.
String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM "
@@ -330,7 +353,8 @@ public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Co
});
}
- public static void deleteCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String codeId)
+ public static void deleteCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
+ String codeId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessCodesTable()
+ " WHERE app_id = ? AND tenant_id = ? AND code_id = ?";
@@ -342,30 +366,35 @@ public static void deleteCode_Transaction(Start start, Connection con, TenantIde
});
}
- public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined)
+ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email,
+ @Nullable String phoneNumber, long timeJoined)
throws StorageTransactionLogicException, StorageQueryException {
return start.startTransaction(con -> {
Connection sqlCon = (Connection) con.getConnection();
try {
{ // app_id_to_user_id
String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable()
- + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)";
+ + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, id);
- pst.setString(3, PASSWORDLESS.toString());
+ pst.setString(3, id);
+ pst.setString(4, PASSWORDLESS.toString());
});
}
{ // all_auth_recipe_users
String QUERY = "INSERT INTO " + getConfig(start).getUsersTable()
- + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)";
+ + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" +
+ " VALUES(?, ?, ?, ?, ?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, id);
- pst.setString(4, PASSWORDLESS.toString());
- pst.setLong(5, timeJoined);
+ pst.setString(4, id);
+ pst.setString(5, PASSWORDLESS.toString());
+ pst.setLong(6, timeJoined);
+ pst.setLong(7, timeJoined);
});
}
@@ -393,16 +422,20 @@ public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier
pst.setString(5, phoneNumber);
});
}
- UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(id, email, phoneNumber, timeJoined));
+ UserInfoPartial userInfo = new UserInfoPartial(id, email, phoneNumber, timeJoined);
+ fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo);
+ fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo);
sqlCon.commit();
- return userInfo;
+ return AuthRecipeUserInfo.create(id, false,
+ userInfo.toLoginMethod());
} catch (SQLException throwables) {
throw new StorageTransactionLogicException(throwables);
}
});
}
- private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connection con, AppIdentifier appIdentifier, String userId)
+ private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier, String userId)
throws StorageQueryException, SQLException {
String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, "
+ "pl_users.phone_number as phone_number, pl_users_to_tenant.tenant_id as tenant_id "
@@ -429,49 +462,59 @@ private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connec
});
}
- public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId)
- throws StorageQueryException, StorageTransactionLogicException {
- start.startTransaction(con -> {
- Connection sqlCon = (Connection) con.getConnection();
- try {
- UserInfoWithTenantId[] userInfos = getUserInfosWithTenant(start, sqlCon, appIdentifier, userId);
+ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier,
+ String userId, boolean deleteUserIdMappingToo)
+ throws StorageQueryException, SQLException {
+ UserInfoWithTenantId[] userInfos = getUserInfosWithTenant_Transaction(start, sqlCon, appIdentifier, userId);
- {
- String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable()
- + " WHERE app_id = ? AND user_id = ?";
+ if (deleteUserIdMappingToo) {
+ String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable()
+ + " WHERE app_id = ? AND user_id = ?";
- update(sqlCon, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
- });
- }
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ } else {
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
- for (UserInfoWithTenantId userInfo : userInfos) {
- if (userInfo.email != null) {
- deleteDevicesByEmail_Transaction(start, sqlCon,
- new TenantIdentifier(
- appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
- userInfo.tenantId),
- userInfo.email);
- }
- if (userInfo.phoneNumber != null) {
- deleteDevicesByPhoneNumber_Transaction(start, sqlCon,
- new TenantIdentifier(
- appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
- userInfo.tenantId),
- userInfo.phoneNumber);
- }
- }
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+ }
- sqlCon.commit();
- } catch (SQLException throwables) {
- throw new StorageTransactionLogicException(throwables);
+ for (UserInfoWithTenantId userInfo : userInfos) {
+ if (userInfo.email != null) {
+ deleteDevicesByEmail_Transaction(start, sqlCon,
+ new TenantIdentifier(
+ appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
+ userInfo.tenantId),
+ userInfo.email);
}
- return null;
- });
+ if (userInfo.phoneNumber != null) {
+ deleteDevicesByPhoneNumber_Transaction(start, sqlCon,
+ new TenantIdentifier(
+ appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
+ userInfo.tenantId),
+ userInfo.phoneNumber);
+ }
+ }
}
- public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String email)
+ public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String userId, String email)
throws SQLException, StorageQueryException {
{
String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable()
@@ -495,7 +538,8 @@ public static int updateUserEmail_Transaction(Start start, Connection con, AppId
}
}
- public static int updateUserPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String phoneNumber)
+ public static int updateUserPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String userId, String phoneNumber)
throws SQLException, StorageQueryException {
{
String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable()
@@ -538,7 +582,8 @@ public static PasswordlessDevice getDevice(Start start, TenantIdentifier tenantI
}
}
- public static PasswordlessDevice[] getDevicesByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email)
+ public static PasswordlessDevice[] getDevicesByEmail(Start start, TenantIdentifier tenantIdentifier,
+ @Nonnull String email)
throws StorageQueryException, SQLException {
String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM "
+ getConfig(start).getPasswordlessDevicesTable()
@@ -585,7 +630,8 @@ public static PasswordlessDevice[] getDevicesByPhoneNumber(Start start, TenantId
});
}
- public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier tenantIdentifier, String deviceIdHash)
+ public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier tenantIdentifier,
+ String deviceIdHash)
throws StorageQueryException, SQLException {
try (Connection con = ConnectionPool.getConnection(start)) {
// We can call the transaction version here because it doesn't lock anything.
@@ -593,7 +639,8 @@ public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier
}
}
- public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier tenantIdentifier, long time) throws StorageQueryException, SQLException {
+ public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier tenantIdentifier, long time)
+ throws StorageQueryException, SQLException {
String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM "
+ getConfig(start).getPasswordlessCodesTable()
+ " WHERE app_id = ? AND tenant_id = ? AND created_at < ?";
@@ -615,7 +662,8 @@ public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier te
});
}
- public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException, SQLException {
+ public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdentifier, String codeId)
+ throws StorageQueryException, SQLException {
String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM "
+ getConfig(start).getPasswordlessCodesTable()
+ " WHERE app_id = ? AND tenant_id = ? AND code_id = ?";
@@ -632,7 +680,8 @@ public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdent
});
}
- public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifier tenantIdentifier, String linkCodeHash)
+ public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifier tenantIdentifier,
+ String linkCodeHash)
throws StorageQueryException, SQLException {
try (Connection con = ConnectionPool.getConnection(start)) {
// We can call the transaction version here because it doesn't lock anything.
@@ -640,28 +689,52 @@ public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifi
}
}
- public static List getUsersByIdList(Start start, AppIdentifier appIdentifier, List ids)
+ public static List getUsersInfoUsingIdList(Start start, Set ids,
+ AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
if (ids.size() > 0) {
// No need to filter based on tenantId because the id list is already filtered for a tenant
- StringBuilder QUERY = new StringBuilder("SELECT user_id, email, phone_number, time_joined "
- + "FROM " + getConfig(start).getPasswordlessUsersTable());
- QUERY.append(" WHERE app_id = ? AND user_id IN (");
- for (int i = 0; i < ids.size(); i++) {
- QUERY.append("?");
- if (i != ids.size() - 1) {
- // not the last element
- QUERY.append(",");
+ String QUERY = "SELECT user_id, email, phone_number, time_joined "
+ + "FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE user_id IN (" +
+ Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?";
+
+ List userInfos = execute(start, QUERY, pst -> {
+ int index = 1;
+ for (String id : ids) {
+ pst.setString(index, id);
+ index++;
}
- }
- QUERY.append(")");
+ pst.setString(index, appIdentifier.getAppId());
+ }, result -> {
+ List finalResult = new ArrayList<>();
+ while (result.next()) {
+ finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result));
+ }
+ return finalResult;
+ });
+ fillUserInfoWithTenantIds(start, appIdentifier, userInfos);
+ fillUserInfoWithVerified(start, appIdentifier, userInfos);
+ return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList());
+ }
+ return Collections.emptyList();
+ }
- List userInfos = execute(start, QUERY.toString(), pst -> {
- pst.setString(1, appIdentifier.getAppId());
- for (int i = 0; i < ids.size(); i++) {
- // i+2 cause this starts with 1 and not 0, and 1 is appId
- pst.setString(i + 2, ids.get(i));
+ public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids,
+ AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
+ if (ids.size() > 0) {
+ // No need to filter based on tenantId because the id list is already filtered for a tenant
+ String QUERY = "SELECT user_id, email, phone_number, time_joined "
+ + "FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE user_id IN (" +
+ Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?";
+
+ List userInfos = execute(con, QUERY, pst -> {
+ int index = 1;
+ for (String id : ids) {
+ pst.setString(index, id);
+ index++;
}
+ pst.setString(index, appIdentifier.getAppId());
}, result -> {
List finalResult = new ArrayList<>();
while (result.next()) {
@@ -669,16 +742,22 @@ public static List getUsersByIdList(Start start, AppIdentifier appIden
}
return finalResult;
});
- return userInfoWithTenantIds(start, appIdentifier, userInfos);
+ fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos);
+ return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList());
}
return Collections.emptyList();
}
- public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException {
+ private static UserInfoPartial getUserById_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
+ String userId)
+ throws StorageQueryException, SQLException {
+ // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id
+ // table
String QUERY = "SELECT user_id, email, phone_number, time_joined FROM "
+ getConfig(start).getPasswordlessUsersTable() + " WHERE app_id = ? AND user_id = ?";
- UserInfoPartial userInfo = execute(start, QUERY, pst -> {
+ return execute(sqlCon, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, userId);
}, result -> {
@@ -687,92 +766,167 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str
}
return null;
});
- return userInfoWithTenantIds(start, appIdentifier, userInfo);
}
- public static UserInfoPartial getUserById(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException {
- // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id table
- String QUERY = "SELECT user_id, email, phone_number, time_joined FROM "
- + getConfig(start).getPasswordlessUsersTable()
- + " WHERE app_id = ? AND user_id = ?";
+ public static List lockEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String email) throws StorageQueryException, SQLException {
+ // normally the query below will use a for update, but sqlite doesn't support it.
+ ((ConnectionWithLocks) con).lock(
+ appIdentifier.getAppId() + "~" + email +
+ Config.getConfig(start).getPasswordlessUsersTable());
+ String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() +
+ " WHERE app_id = ? AND email = ?";
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, email);
+ }, result -> {
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
+ }
+ return userIds;
+ });
+ }
- return execute(sqlCon, QUERY, pst -> {
+ public static List lockPhone_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier,
+ String phoneNumber)
+ throws SQLException, StorageQueryException {
+ // normally the query below will use a for update, but sqlite doesn't support it.
+ ((ConnectionWithLocks) con).lock(
+ appIdentifier.getAppId() + "~" + phoneNumber +
+ Config.getConfig(start).getPasswordlessUsersTable());
+
+ String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() +
+ " WHERE app_id = ? AND phone_number = ?";
+ return execute(con, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
+ pst.setString(2, phoneNumber);
}, result -> {
- if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
}
- return null;
+ return userIds;
});
}
- public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email)
+ public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier,
+ String email)
throws StorageQueryException, SQLException {
- String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, "
- + "pl_users.phone_number as phone_number, pl_users.time_joined as time_joined "
- + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant "
- + "JOIN " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users "
- + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id "
- + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.email = ? ";
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pless" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" +
+ " WHERE pless.app_id = ? AND pless.tenant_id = ? AND pless.email = ?";
- UserInfoPartial userInfo = execute(start, QUERY, pst -> {
+ return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, email);
}, result -> {
if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ return result.getString("user_id");
}
return null;
});
- return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo);
}
- public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber)
+ public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String email)
throws StorageQueryException, SQLException {
- String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, "
- + "pl_users.phone_number as phone_number, pl_users.time_joined as time_joined "
- + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant "
- + "JOIN " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users "
- + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id "
- + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.phone_number = ? ";
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" +
+ " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" +
+ " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" +
+ " WHERE pless.app_id = ? AND pless.email = ?";
- UserInfoPartial userInfo = execute(start, QUERY, pst -> {
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, email);
+ }, result -> {
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
+ }
+ return userIds;
+ });
+ }
+
+ public static String getPrimaryUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier,
+ @Nonnull String phoneNumber)
+ throws StorageQueryException, SQLException {
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pless" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" +
+ " WHERE pless.app_id = ? AND pless.tenant_id = ? AND pless.phone_number = ?";
+
+ return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, phoneNumber);
}, result -> {
if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ return result.getString("user_id");
}
return null;
});
- return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo);
}
- public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId)
+ public static List listUserIdsByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ @Nonnull String phoneNumber)
throws StorageQueryException, SQLException {
- UserInfoPartial userInfo = PasswordlessQueries.getUserById(start, sqlCon,
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" +
+ " WHERE pless.app_id = ? AND pless.phone_number = ?";
+
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, phoneNumber);
+ }, result -> {
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
+ }
+ return userIds;
+ });
+ }
+
+ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon,
+ TenantIdentifier tenantIdentifier, String userId)
+ throws StorageQueryException, SQLException, UnknownUserIdException {
+ UserInfoPartial userInfo = PasswordlessQueries.getUserById_Transaction(start, sqlCon,
tenantIdentifier.toAppIdentifier(), userId);
+ if (userInfo == null) {
+ throw new UnknownUserIdException();
+ }
+
+ GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId);
+
{ // all_auth_recipe_users
String QUERY = "INSERT INTO " + getConfig(start).getUsersTable()
- + "(app_id, tenant_id, user_id, recipe_id, time_joined)"
- + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
+ + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)"
+ + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userInfo.id);
- pst.setString(4, PASSWORDLESS.toString());
- pst.setLong(5, userInfo.timeJoined);
+ pst.setString(4, accountLinkingInfo.primaryUserId);
+ pst.setBoolean(5, accountLinkingInfo.isLinked);
+ pst.setString(6, PASSWORDLESS.toString());
+ pst.setLong(7, userInfo.timeJoined);
+ pst.setLong(8, userInfo.timeJoined);
});
}
{ // passwordless_user_to_tenant
String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable()
+ "(app_id, tenant_id, user_id, email, phone_number)"
- + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
+ + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT (app_id, tenant_id, user_id) DO NOTHING";
int numRows = update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
@@ -786,7 +940,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC
}
}
- public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId)
+ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon,
+ TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
{ // all_auth_recipe_users
String QUERY = "DELETE FROM " + getConfig(start).getUsersTable()
@@ -804,42 +959,124 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection
// automatically deleted from passwordless_user_to_tenant because of foreign key constraint
}
- private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo)
+ private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start,
+ Connection sqlCon,
+ AppIdentifier appIdentifier,
+ UserInfoPartial userInfo)
throws SQLException, StorageQueryException {
if (userInfo == null) return null;
- try (Connection con = ConnectionPool.getConnection(start)) {
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0);
+ return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0);
+ }
+
+ private static List fillUserInfoWithVerified_transaction(Start start,
+ Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userInfos)
+ throws SQLException, StorageQueryException {
+ List userIdsAndEmails = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ if (userInfo.email == null) {
+ // phone number, so we mark it as verified
+ userInfo.verified = true;
+ } else {
+ userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email));
+ }
+ }
+ List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon,
+ appIdentifier,
+ userIdsAndEmails);
+ Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified);
+ for (UserInfoPartial userInfo : userInfos) {
+ if (userInfo.verified != null) {
+ // this means phone number
+ assert (userInfo.email == null);
+ continue;
+ }
+ if (verifiedUserIdsSet.contains(userInfo.id)) {
+ userInfo.verified = true;
+ } else {
+ userInfo.verified = false;
+ }
}
+ return userInfos;
}
- private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos)
+ private static List fillUserInfoWithVerified(Start start,
+ AppIdentifier appIdentifier,
+ List userInfos)
throws SQLException, StorageQueryException {
- try (Connection con = ConnectionPool.getConnection(start)) {
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ List userIdsAndEmails = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ if (userInfo.email == null) {
+ // phone number, so we mark it as verified
+ userInfo.verified = true;
+ } else {
+ userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email));
+ }
}
+ List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified(start,
+ appIdentifier,
+ userIdsAndEmails);
+ Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified);
+ for (UserInfoPartial userInfo : userInfos) {
+ if (userInfo.verified != null) {
+ // this means phone number
+ assert (userInfo.email == null);
+ continue;
+ }
+ if (verifiedUserIdsSet.contains(userInfo.id)) {
+ userInfo.verified = true;
+ } else {
+ userInfo.verified = false;
+ }
+ }
+ return userInfos;
}
- private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo)
+ private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ UserInfoPartial userInfo)
throws SQLException, StorageQueryException {
if (userInfo == null) return null;
- return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0);
+ return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0);
}
- private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos)
+ private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userInfos)
throws SQLException, StorageQueryException {
String[] userIds = new String[userInfos.size()];
for (int i = 0; i < userInfos.size(); i++) {
userIds[i] = userInfos.get(i).id;
}
- Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, appIdentifier, userIds);
- List result = new ArrayList<>();
+ Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon,
+ appIdentifier,
+ userIds);
+ List result = new ArrayList<>();
for (UserInfoPartial userInfo : userInfos) {
- result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.phoneNumber, userInfo.timeJoined,
- tenantIdsForUserIds.get(userInfo.id).toArray(new String[0])));
+ userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]);
+ }
+ return userInfos;
+ }
+
+ private static List fillUserInfoWithTenantIds(Start start,
+ AppIdentifier appIdentifier,
+ List userInfos)
+ throws SQLException, StorageQueryException {
+ String[] userIds = new String[userInfos.size()];
+ for (int i = 0; i < userInfos.size(); i++) {
+ userIds[i] = userInfos.get(i).id;
}
- return result;
+ Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds(start,
+ appIdentifier,
+ userIds);
+ List result = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]);
+ }
+ return userInfos;
}
private static class PasswordlessDeviceRowMapper implements RowMapper {
@@ -882,6 +1119,9 @@ private static class UserInfoPartial {
public final long timeJoined;
public final String email;
public final String phoneNumber;
+ public String[] tenantIds;
+ public Boolean verified;
+ public Boolean isPrimary;
UserInfoPartial(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) {
this.id = id.trim();
@@ -894,6 +1134,13 @@ private static class UserInfoPartial {
this.email = email;
this.phoneNumber = phoneNumber;
}
+
+ public LoginMethod toLoginMethod() {
+ assert (tenantIds != null);
+ assert (verified != null);
+ return new LoginMethod(id, timeJoined, verified, new LoginMethod.PasswordlessInfo(email, phoneNumber),
+ tenantIds);
+ }
}
private static class UserInfoRowMapper implements RowMapper {
@@ -913,7 +1160,6 @@ public UserInfoPartial map(ResultSet result) throws Exception {
}
}
-
private static class UserInfoWithTenantId {
public final String userId;
public final String tenantId;
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java
index 357985eeb..65fa18c1a 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java
@@ -19,7 +19,6 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.supertokens.inmemorydb.ConnectionWithLocks;
-import io.supertokens.inmemorydb.QueryExecutorTemplate;
import io.supertokens.inmemorydb.Start;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.KeyValueInfo;
@@ -36,7 +35,6 @@
import java.util.ArrayList;
import java.util.List;
-import static io.supertokens.inmemorydb.PreparedStatementValueSetter.NO_OP_SETTER;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
import static io.supertokens.inmemorydb.config.Config.getConfig;
@@ -105,21 +103,46 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con
String sessionHandle)
throws SQLException, StorageQueryException {
- ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + sessionHandle + Config.getConfig(start).getSessionInfoTable());
-
- String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, "
- + "created_at_time, jwt_user_payload, use_static_key FROM " + getConfig(start).getSessionInfoTable()
- + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?";
- return execute(con, QUERY, pst -> {
+ ((ConnectionWithLocks) con).lock(
+ tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + sessionHandle +
+ Config.getConfig(start).getSessionInfoTable());
+ // we do this as two separate queries and not one query with left join cause psql does not
+ // support left join with for update if the right table returns null.
+ String QUERY =
+ "SELECT session_handle, user_id, refresh_token_hash_2, session_data, " +
+ "expires_at, created_at_time, jwt_user_payload, use_static_key FROM " +
+ getConfig(start).getSessionInfoTable()
+ + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?";
+ SessionInfo sessionInfo = execute(con, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, sessionHandle);
}, result -> {
if (result.next()) {
- return SessionInfoRowMapper.getInstance().mapOrThrow(result);
+ return SessionInfoRowMapper.getInstance().mapOrThrow(result, false);
}
return null;
});
+
+ if (sessionInfo == null) {
+ return null;
+ }
+
+ QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, tenantIdentifier.getAppId());
+ pst.setString(2, sessionInfo.recipeUserId);
+ }, result -> {
+ if (result.next()) {
+ String primaryUserId = result.getString("primary_or_recipe_user_id");
+ if (primaryUserId != null) {
+ sessionInfo.userId = primaryUserId;
+ }
+ }
+ return sessionInfo;
+ });
}
public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
@@ -191,6 +214,18 @@ public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier
});
}
+ public static void deleteSessionsOfUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier,
+ String userId)
+ throws SQLException, StorageQueryException {
+ String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable()
+ + " WHERE app_id = ? AND user_id = ?";
+
+ update(sqlCon, QUERY.toString(), pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+
public static boolean deleteSessionsOfUser(Start start, TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable()
@@ -294,16 +329,24 @@ public static int updateSession(Start start, TenantIdentifier tenantIdentifier,
public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle)
throws SQLException, StorageQueryException {
- String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, "
- + "created_at_time, jwt_user_payload, use_static_key FROM " + getConfig(start).getSessionInfoTable()
- + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?";
+ String QUERY =
+ "SELECT sess.session_handle, sess.user_id, sess.refresh_token_hash_2, sess.session_data, sess" +
+ ".expires_at, "
+ +
+ "sess.created_at_time, sess.jwt_user_payload, sess.use_static_key, users" +
+ ".primary_or_recipe_user_id FROM " +
+ getConfig(start).getSessionInfoTable()
+ + " AS sess LEFT JOIN " + getConfig(start).getUsersTable() +
+ " as users ON sess.app_id = users.app_id AND sess.user_id = users.user_id WHERE sess.app_id =" +
+ " ? AND " +
+ "sess.tenant_id = ? AND sess.session_handle = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, sessionHandle);
}, result -> {
if (result.next()) {
- return SessionInfoRowMapper.getInstance().mapOrThrow(result);
+ return SessionInfoRowMapper.getInstance().mapOrThrow(result, true);
}
return null;
});
@@ -326,7 +369,8 @@ public static void addAccessTokenSigningKey_Transaction(Start start, Connection
public static KeyValueInfo[] getAccessTokenSigningKeys_Transaction(Start start, Connection con,
AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
- ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + Config.getConfig(start).getAccessTokenSigningKeysTable());
+ ((ConnectionWithLocks) con).lock(
+ appIdentifier.getAppId() + Config.getConfig(start).getAccessTokenSigningKeysTable());
String QUERY = "SELECT * FROM " + getConfig(start).getAccessTokenSigningKeysTable()
+ " WHERE app_id = ?";
@@ -357,7 +401,7 @@ public static void removeAccessTokenSigningKeysBefore(Start start, AppIdentifier
});
}
- static class SessionInfoRowMapper implements RowMapper {
+ static class SessionInfoRowMapper {
public static final SessionInfoRowMapper INSTANCE = new SessionInfoRowMapper();
private SessionInfoRowMapper() {
@@ -367,14 +411,23 @@ private static SessionInfoRowMapper getInstance() {
return INSTANCE;
}
- @Override
- public SessionInfo map(ResultSet result) throws Exception {
+ public SessionInfo mapOrThrow(ResultSet result, boolean hasPrimaryOrRecipeUserId) throws StorageQueryException {
JsonParser jp = new JsonParser();
- return new SessionInfo(result.getString("session_handle"), result.getString("user_id"),
- result.getString("refresh_token_hash_2"),
- jp.parse(result.getString("session_data")).getAsJsonObject(), result.getLong("expires_at"),
- jp.parse(result.getString("jwt_user_payload")).getAsJsonObject(),
- result.getLong("created_at_time"), result.getBoolean("use_static_key"));
+ // if result.getString("primary_or_recipe_user_id") is null, it will be handled by SessionInfo
+ // constructor
+ try {
+ return new SessionInfo(result.getString("session_handle"),
+ hasPrimaryOrRecipeUserId ? result.getString("primary_or_recipe_user_id") :
+ result.getString("user_id"),
+ result.getString("user_id"),
+ result.getString("refresh_token_hash_2"),
+ jp.parse(result.getString("session_data")).getAsJsonObject(),
+ result.getLong("expires_at"),
+ jp.parse(result.getString("jwt_user_payload")).getAsJsonObject(),
+ result.getLong("created_at_time"), result.getBoolean("use_static_key"));
+ } catch (Exception e) {
+ throw new StorageQueryException(e);
+ }
}
}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java
index 5ef6ee4d8..3e3848353 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java
@@ -19,19 +19,22 @@
import io.supertokens.inmemorydb.ConnectionPool;
import io.supertokens.inmemorydb.ConnectionWithLocks;
import io.supertokens.inmemorydb.Start;
+import io.supertokens.inmemorydb.Utils;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.RowMapper;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
-import io.supertokens.pluginInterface.thirdparty.UserInfo;
-import org.jetbrains.annotations.NotNull;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
+import java.util.stream.Collectors;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
@@ -76,35 +79,41 @@ static String getQueryToCreateThirdPartyUserToTenantTable(Start start) {
+ "UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id),"
+ "PRIMARY KEY (app_id, tenant_id, user_id),"
+ "FOREIGN KEY (app_id, tenant_id, user_id)"
- + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE"
+ + " REFERENCES " + Config.getConfig(start).getUsersTable() +
+ "(app_id, tenant_id, user_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
- public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined)
+ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email,
+ LoginMethod.ThirdParty thirdParty, long timeJoined)
throws StorageQueryException, StorageTransactionLogicException {
return start.startTransaction(con -> {
Connection sqlCon = (Connection) con.getConnection();
try {
{ // app_id_to_user_id
String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable()
- + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)";
+ + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, id);
- pst.setString(3, THIRD_PARTY.toString());
+ pst.setString(3, id);
+ pst.setString(4, THIRD_PARTY.toString());
});
}
{ // all_auth_recipe_users
String QUERY = "INSERT INTO " + getConfig(start).getUsersTable()
- + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)";
+ + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" +
+ " VALUES(?, ?, ?, ?, ?, ?, ?)";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, id);
- pst.setString(4, THIRD_PARTY.toString());
- pst.setLong(5, timeJoined);
+ pst.setString(4, id);
+ pst.setString(5, THIRD_PARTY.toString());
+ pst.setLong(6, timeJoined);
+ pst.setLong(7, timeJoined);
});
}
@@ -135,9 +144,11 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St
});
}
- UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(id, email, thirdParty, timeJoined));
+ UserInfoPartial userInfo = new UserInfoPartial(id, email, thirdParty, timeJoined);
+ fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo);
+ fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo);
sqlCon.commit();
- return userInfo;
+ return AuthRecipeUserInfo.create(id, false, userInfo.toLoginMethod());
} catch (SQLException throwables) {
throw new StorageTransactionLogicException(throwables);
@@ -145,70 +156,136 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St
});
}
- public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId)
- throws StorageQueryException, StorageTransactionLogicException {
- start.startTransaction(con -> {
- Connection sqlCon = (Connection) con.getConnection();
- try {
- {
- String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable()
- + " WHERE app_id = ? AND user_id = ?";
+ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier,
+ String userId, boolean deleteUserIdMappingToo)
+ throws StorageQueryException, SQLException {
+ if (deleteUserIdMappingToo) {
+ String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable()
+ + " WHERE app_id = ? AND user_id = ?";
- update(sqlCon, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
- });
- }
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ } else {
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
- sqlCon.commit();
- } catch (SQLException throwables) {
- throw new StorageTransactionLogicException(throwables);
+ {
+ String QUERY = "DELETE FROM " + getConfig(start).getThirdPartyUsersTable()
+ + " WHERE app_id = ? AND user_id = ?";
+ update(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
}
- return null;
+ }
+ }
+
+ public static List lockEmail_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier,
+ String email) throws SQLException, StorageQueryException {
+ // normally the query below will use a for update, but sqlite doesn't support it.
+ ((ConnectionWithLocks) con).lock(
+ appIdentifier.getAppId() + "~" + email +
+ Config.getConfig(start).getThirdPartyUsersTable());
+
+ String QUERY = "SELECT tp.user_id as user_id "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" +
+ " WHERE tp.app_id = ? AND tp.email = ?";
+
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, email);
+ }, result -> {
+ List finalResult = new ArrayList<>();
+ while (result.next()) {
+ finalResult.add(result.getString("user_id"));
+ }
+ return finalResult;
});
}
- public static UserInfo getThirdPartyUserInfoUsingId(Start start, AppIdentifier appIdentifier, String userId)
+ public static List lockThirdPartyInfo_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier,
+ String thirdPartyId, String thirdPartyUserId)
throws SQLException, StorageQueryException {
- String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM "
- + getConfig(start).getThirdPartyUsersTable() + " WHERE app_id = ? AND user_id = ?";
+ // normally the query below will use a for update, but sqlite doesn't support it.
+ ((ConnectionWithLocks) con).lock(
+ appIdentifier.getAppId() + "~" + thirdPartyId + thirdPartyUserId +
+ Config.getConfig(start).getThirdPartyUsersTable());
+
+ // in psql / mysql dbs, this will lock the rows that are in both the tables that meet the ON criteria only.
+ String QUERY = "SELECT user_id " +
+ " FROM " + getConfig(start).getThirdPartyUsersTable() +
+ " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ?";
- UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> {
+ return execute(con, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, userId);
+ pst.setString(2, thirdPartyId);
+ pst.setString(3, thirdPartyUserId);
}, result -> {
- if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ List finalResult = new ArrayList<>();
+ while (result.next()) {
+ finalResult.add(result.getString("user_id"));
}
- return null;
+ return finalResult;
});
- return userInfoWithTenantIds(start, appIdentifier, userInfo);
}
- public static List getUsersInfoUsingIdList(Start start, AppIdentifier appIdentifier, List ids)
+ public static List getUsersInfoUsingIdList(Start start, Set ids,
+ AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
if (ids.size() > 0) {
- // No need to filter based on tenantId because the id list is already filtered for a tenant
- StringBuilder QUERY = new StringBuilder(
- "SELECT user_id, third_party_id, third_party_user_id, email, time_joined "
- + "FROM " + getConfig(start).getThirdPartyUsersTable());
- QUERY.append(" WHERE app_id = ? AND user_id IN (");
- for (int i = 0; i < ids.size(); i++) {
-
- QUERY.append("?");
- if (i != ids.size() - 1) {
- // not the last element
- QUERY.append(",");
+ String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id IN (" +
+ Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?";
+
+ List userInfos = execute(start, QUERY, pst -> {
+ int index = 1;
+ for (String id : ids) {
+ pst.setString(index, id);
+ index++;
+ }
+ pst.setString(index, appIdentifier.getAppId());
+ }, result -> {
+ List finalResult = new ArrayList<>();
+ while (result.next()) {
+ finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result));
}
+ return finalResult;
+ });
+
+ try (Connection con = ConnectionPool.getConnection(start)) {
+ fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos);
}
- QUERY.append(")");
+ return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList());
+ }
+ return Collections.emptyList();
+ }
- List userInfos = execute(start, QUERY.toString(), pst -> {
- pst.setString(1, appIdentifier.getAppId());
- for (int i = 0; i < ids.size(); i++) {
- // i+2 cause this starts with 1 and not 0, and 1 is appId
- pst.setString(i + 2, ids.get(i));
+ public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids,
+ AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
+ if (ids.size() > 0) {
+ String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id IN (" +
+ Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?";
+
+ List userInfos = execute(con, QUERY, pst -> {
+ int index = 1;
+ for (String id : ids) {
+ pst.setString(index, id);
+ index++;
}
+ pst.setString(index, appIdentifier.getAppId());
}, result -> {
List finalResult = new ArrayList<>();
while (result.next()) {
@@ -216,36 +293,82 @@ public static List getUsersInfoUsingIdList(Start start, AppIdentifier
}
return finalResult;
});
- return userInfoWithTenantIds(start, appIdentifier, userInfos);
+
+ fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos);
+ return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList());
}
return Collections.emptyList();
}
- public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifier tenantIdentifier,
- String thirdPartyId, String thirdPartyUserId)
+
+ public static List listUserIdsByThirdPartyInfo(Start start, AppIdentifier appIdentifier,
+ String thirdPartyId, String thirdPartyUserId)
throws SQLException, StorageQueryException {
- String QUERY = "SELECT tp_users.user_id as user_id, tp_users.third_party_id as third_party_id, "
- + "tp_users.third_party_user_id as third_party_user_id, tp_users.email as email, "
- + "tp_users.time_joined as time_joined "
- + "FROM " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_users_to_tenant "
- + "JOIN " + getConfig(start).getThirdPartyUsersTable() + " AS tp_users "
- + "ON tp_users.app_id = tp_users_to_tenant.app_id AND tp_users.user_id = tp_users_to_tenant.user_id "
- + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? "
- + "AND tp_users_to_tenant.third_party_id = ? AND tp_users_to_tenant.third_party_user_id = ?";
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" +
+ " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?";
- UserInfoPartial userInfo = execute(start, QUERY, pst -> {
+ return execute(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, thirdPartyId);
+ pst.setString(3, thirdPartyUserId);
+ }, result -> {
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
+ }
+ return userIds;
+ });
+ }
+
+ public static List listUserIdsByThirdPartyInfo_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String thirdPartyId, String thirdPartyUserId)
+ throws SQLException, StorageQueryException {
+
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" +
+ " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?";
+
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, thirdPartyId);
+ pst.setString(3, thirdPartyUserId);
+ }, result -> {
+ List userIds = new ArrayList<>();
+ while (result.next()) {
+ userIds.add(result.getString("user_id"));
+ }
+ return userIds;
+ });
+ }
+
+ public static String getUserIdByThirdPartyInfo(Start start, TenantIdentifier tenantIdentifier,
+ String thirdPartyId, String thirdPartyUserId)
+ throws SQLException, StorageQueryException {
+
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" +
+ " WHERE tp.app_id = ? AND tp.tenant_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?";
+
+ return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, thirdPartyId);
pst.setString(4, thirdPartyUserId);
}, result -> {
if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
+ return result.getString("user_id");
}
return null;
});
- return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo);
}
public static void updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
@@ -262,34 +385,12 @@ public static void updateUserEmail_Transaction(Start start, Connection con, AppI
});
}
- public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con,
- AppIdentifier appIdentifier, String thirdPartyId,
- String thirdPartyUserId)
- throws SQLException, StorageQueryException {
-
- ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + thirdPartyId + "~" + thirdPartyUserId + Config.getConfig(start).getThirdPartyUsersTable());
-
- String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM "
- + getConfig(start).getThirdPartyUsersTable()
- + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ?";
- UserInfoPartial userInfo = execute(con, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, thirdPartyId);
- pst.setString(3, thirdPartyUserId);
- }, result -> {
- if (result.next()) {
- return UserInfoRowMapper.getInstance().mapOrThrow(result);
- }
- return null;
- });
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfo);
- }
-
- private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection con,
+ private static UserInfoPartial getUserInfoUsingUserId_Transaction(Start start, Connection con,
AppIdentifier appIdentifier, String userId)
throws SQLException, StorageQueryException {
- // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id table
+ // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id
+ // table
String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM "
+ getConfig(start).getThirdPartyUsersTable()
+ " WHERE app_id = ? AND user_id = ?";
@@ -304,55 +405,83 @@ private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection co
});
}
- public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier tenantIdentifier,
- @NotNull String email)
- throws SQLException, StorageQueryException {
-
- String QUERY = "SELECT tp_users.user_id as user_id, tp_users.third_party_id as third_party_id, "
- + "tp_users.third_party_user_id as third_party_user_id, tp_users.email as email, "
- + "tp_users.time_joined as time_joined "
- + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp_users "
- + "JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_users_to_tenant "
- + "ON tp_users.app_id = tp_users_to_tenant.app_id AND tp_users.user_id = tp_users_to_tenant.user_id "
- + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? AND tp_users.email = ? "
- + "ORDER BY time_joined";
-
- List userInfos = execute(start, QUERY.toString(), pst -> {
+ public static List getPrimaryUserIdUsingEmail(Start start,
+ TenantIdentifier tenantIdentifier, String email)
+ throws StorageQueryException, SQLException {
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" +
+ " JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
+ " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" +
+ " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_tenants" +
+ " ON tp_tenants.app_id = all_users.app_id AND tp_tenants.user_id = all_users.user_id" +
+ " WHERE tp.app_id = ? AND tp_tenants.tenant_id = ? AND tp.email = ?";
+
+ return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, email);
}, result -> {
- List finalResult = new ArrayList<>();
+ List finalResult = new ArrayList<>();
while (result.next()) {
- finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result));
+ finalResult.add(result.getString("user_id"));
}
return finalResult;
});
- return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfos).toArray(new UserInfo[0]);
}
- public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId)
- throws SQLException, StorageQueryException {
- UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId(start, sqlCon,
+ public static List getPrimaryUserIdUsingEmail_Transaction(Start start, Connection con,
+ AppIdentifier appIdentifier, String email)
+ throws StorageQueryException, SQLException {
+ String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" +
+ " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" +
+ " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" +
+ " WHERE tp.app_id = ? AND tp.email = ?";
+
+ return execute(con, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, email);
+ }, result -> {
+ List finalResult = new ArrayList<>();
+ while (result.next()) {
+ finalResult.add(result.getString("user_id"));
+ }
+ return finalResult;
+ });
+ }
+
+ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon,
+ TenantIdentifier tenantIdentifier, String userId)
+ throws SQLException, StorageQueryException, UnknownUserIdException {
+ UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId_Transaction(start, sqlCon,
tenantIdentifier.toAppIdentifier(), userId);
+ if (userInfo == null) {
+ throw new UnknownUserIdException();
+ }
+
+ GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId);
+
{ // all_auth_recipe_users
String QUERY = "INSERT INTO " + getConfig(start).getUsersTable()
- + "(app_id, tenant_id, user_id, recipe_id, time_joined)"
- + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
+ + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)"
+ + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userInfo.id);
- pst.setString(4, THIRD_PARTY.toString());
- pst.setLong(5, userInfo.timeJoined);
+ pst.setString(4, accountLinkingInfo.primaryUserId);
+ pst.setBoolean(5, accountLinkingInfo.isLinked);
+ pst.setString(6, THIRD_PARTY.toString());
+ pst.setLong(7, userInfo.timeJoined);
+ pst.setLong(8, userInfo.timeJoined);
});
}
{ // thirdparty_user_to_tenant
String QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUserToTenantTable()
+ "(app_id, tenant_id, user_id, third_party_id, third_party_user_id)"
- + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING";
+ + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT (app_id, tenant_id, user_id) DO NOTHING";
int numRows = update(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
@@ -365,7 +494,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC
}
}
- public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId)
+ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon,
+ TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
{ // all_auth_recipe_users
String QUERY = "DELETE FROM " + getConfig(start).getUsersTable()
@@ -383,56 +513,84 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection
// automatically deleted from thirdparty_user_to_tenant because of foreign key constraint
}
- private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo)
+ private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ UserInfoPartial userInfo)
throws SQLException, StorageQueryException {
if (userInfo == null) return null;
- try (Connection con = ConnectionPool.getConnection(start)) {
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0);
- }
+ return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0);
}
- private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos)
+ private static List fillUserInfoWithVerified_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userInfos)
throws SQLException, StorageQueryException {
- try (Connection con = ConnectionPool.getConnection(start)) {
- return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos);
+ List userIdsAndEmails = new ArrayList<>();
+ for (UserInfoPartial userInfo : userInfos) {
+ userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email));
+ }
+ List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon,
+ appIdentifier,
+ userIdsAndEmails);
+ Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified);
+ for (UserInfoPartial userInfo : userInfos) {
+ if (verifiedUserIdsSet.contains(userInfo.id)) {
+ userInfo.verified = true;
+ } else {
+ userInfo.verified = false;
+ }
}
+ return userInfos;
}
- private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo)
+ private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ UserInfoPartial userInfo)
throws SQLException, StorageQueryException {
if (userInfo == null) return null;
- return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0);
+ return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0);
}
- private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos)
+ private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier,
+ List userInfos)
throws SQLException, StorageQueryException {
String[] userIds = new String[userInfos.size()];
for (int i = 0; i < userInfos.size(); i++) {
userIds[i] = userInfos.get(i).id;
}
- Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, appIdentifier, userIds);
- List result = new ArrayList<>();
+ Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon,
+ appIdentifier,
+ userIds);
for (UserInfoPartial userInfo : userInfos) {
- result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.thirdParty, userInfo.timeJoined,
- tenantIdsForUserIds.get(userInfo.id).toArray(new String[0])));
+ userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]);
}
-
- return result;
+ return userInfos;
}
private static class UserInfoPartial {
public final String id;
public final String email;
- public final UserInfo.ThirdParty thirdParty;
+ public final LoginMethod.ThirdParty thirdParty;
public final long timeJoined;
+ public String[] tenantIds;
+ public Boolean verified;
+ public Boolean isPrimary;
- public UserInfoPartial(String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) {
+ public UserInfoPartial(String id, String email, LoginMethod.ThirdParty thirdParty, long timeJoined) {
this.id = id.trim();
this.email = email;
this.thirdParty = thirdParty;
this.timeJoined = timeJoined;
}
+
+ public LoginMethod toLoginMethod() {
+ assert (tenantIds != null);
+ assert (verified != null);
+ return new LoginMethod(id, timeJoined, verified, email,
+ new LoginMethod.ThirdParty(thirdParty.id, thirdParty.userId), tenantIds);
+ }
}
private static class UserInfoRowMapper implements RowMapper {
@@ -448,7 +606,7 @@ private static UserInfoRowMapper getInstance() {
@Override
public UserInfoPartial map(ResultSet result) throws Exception {
return new UserInfoPartial(result.getString("user_id"), result.getString("email"),
- new UserInfo.ThirdParty(result.getString("third_party_id"),
+ new LoginMethod.ThirdParty(result.getString("third_party_id"),
result.getString("third_party_user_id")),
result.getLong("time_joined"));
}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java
index fd7831c75..2bfad8270 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java
@@ -24,6 +24,7 @@
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import javax.annotation.Nullable;
+import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
@@ -115,6 +116,25 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter
}
+ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(Start start, Connection sqlCon,
+ AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException {
+ String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable()
+ + " WHERE app_id = ? AND (supertokens_user_id = ? OR external_user_id = ?)";
+
+ return execute(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ pst.setString(3, userId);
+ }, result -> {
+ ArrayList userIdMappingArray = new ArrayList<>();
+ while (result.next()) {
+ userIdMappingArray.add(UserIdMappingRowMapper.getInstance().mapOrThrow(result));
+ }
+ return userIdMappingArray.toArray(UserIdMapping[]::new);
+ });
+
+ }
+
public static HashMap getUserIdMappingWithUserIds(Start start, ArrayList userIds)
throws SQLException, StorageQueryException {
@@ -205,6 +225,37 @@ public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start s
return rowUpdated > 0;
}
+ public static UserIdMapping getuseraIdMappingWithSuperTokensUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
+ String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable()
+ + " WHERE app_id = ? AND supertokens_user_id = ?";
+ return execute(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ }, result -> {
+ if (result.next()) {
+ return UserIdMappingRowMapper.getInstance().mapOrThrow(result);
+ }
+ return null;
+ });
+ }
+
+ public static UserIdMapping getUserIdMappingWithExternalUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
+ String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable()
+ + " WHERE app_id = ? AND external_user_id = ?";
+
+ return execute(sqlCon, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ }, result -> {
+ if (result.next()) {
+ return UserIdMappingRowMapper.getInstance().mapOrThrow(result);
+ }
+ return null;
+ });
+ }
+
private static class UserIdMappingRowMapper implements RowMapper {
private static final UserIdMappingRowMapper INSTANCE = new UserIdMappingRowMapper();
@@ -221,4 +272,5 @@ public UserIdMapping map(ResultSet rs) throws Exception {
rs.getString("external_user_id_info"));
}
}
-}
\ No newline at end of file
+
+}
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java
index 2fb93b620..bbdb4d9c3 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java
@@ -48,7 +48,8 @@ public static String getQueryToCreateUserMetadataTable(Start start) {
// @formatter:on
}
- public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException {
+ public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable()
+ " WHERE app_id = ? AND user_id = ?";
@@ -58,7 +59,20 @@ public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, S
});
}
- public static int setUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, JsonObject metadata)
+ public static int deleteUserMetadata_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier,
+ String userId)
+ throws SQLException, StorageQueryException {
+ String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable()
+ + " WHERE app_id = ? AND user_id = ?";
+
+ return update(sqlCon, QUERY.toString(), pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, userId);
+ });
+ }
+
+ public static int setUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String userId, JsonObject metadata)
throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + getConfig(start).getUserMetadataTable()
@@ -91,7 +105,8 @@ public static JsonObject getUserMetadata_Transaction(Start start, Connection con
});
}
- public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException {
+ public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable()
+ " WHERE app_id = ? AND user_id = ?";
return execute(start, QUERY, pst -> {
diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserRoleQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserRolesQueries.java
similarity index 88%
rename from src/main/java/io/supertokens/inmemorydb/queries/UserRoleQueries.java
rename to src/main/java/io/supertokens/inmemorydb/queries/UserRolesQueries.java
index 028a1e459..dc7fb1710 100644
--- a/src/main/java/io/supertokens/inmemorydb/queries/UserRoleQueries.java
+++ b/src/main/java/io/supertokens/inmemorydb/queries/UserRolesQueries.java
@@ -33,7 +33,7 @@
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
import static io.supertokens.inmemorydb.config.Config.getConfig;
-public class UserRoleQueries {
+public class UserRolesQueries {
public static String getQueryToCreateRolesTable(Start start) {
String tableName = Config.getConfig(start).getRolesTable();
// @formatter:off
@@ -101,7 +101,8 @@ public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start,
public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start start, Connection con,
AppIdentifier appIdentifier, String role,
- String permission) throws SQLException, StorageQueryException {
+ String permission)
+ throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + getConfig(start).getUserRolesPermissionsTable()
+ " (app_id, role, permission) VALUES(?, ?, ?) ON CONFLICT DO NOTHING";
@@ -112,32 +113,18 @@ public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start star
});
}
- public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException {
-
- try {
- return start.startTransaction(con -> {
- // Row lock must be taken to delete the role, otherwise the table may be locked for delete
- Connection sqlCon = (Connection) con.getConnection();
- ((ConnectionWithLocks) sqlCon).lock(appIdentifier.getAppId() + "~" + role + Config.getConfig(start).getRolesTable());
-
- String QUERY = "DELETE FROM " + getConfig(start).getRolesTable()
- + " WHERE app_id = ? AND role = ? ;";
-
- try {
- return update(sqlCon, QUERY, pst -> {
- pst.setString(1, appIdentifier.getAppId());
- pst.setString(2, role);
- }) == 1;
- } catch (SQLException e) {
- throw new StorageTransactionLogicException(e);
- }
- });
- } catch (StorageTransactionLogicException e) {
- throw new StorageQueryException(e.actualException);
- }
+ public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role)
+ throws SQLException, StorageQueryException {
+ String QUERY = "DELETE FROM " + getConfig(start).getRolesTable()
+ + " WHERE app_id = ? AND role = ? ;";
+ return update(start, QUERY, pst -> {
+ pst.setString(1, appIdentifier.getAppId());
+ pst.setString(2, role);
+ }) == 1;
}
- public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException {
+ public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, String role)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable()
+ " WHERE app_id = ? AND role = ?";
return execute(start, QUERY, pst -> {
@@ -146,7 +133,8 @@ public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, St
}, ResultSet::next);
}
- public static String[] getPermissionsForRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException {
+ public static String[] getPermissionsForRole(Start start, AppIdentifier appIdentifier, String role)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT permission FROM " + Config.getConfig(start).getUserRolesPermissionsTable()
+ " WHERE app_id = ? AND role = ?;";
return execute(start, QUERY, pst -> {
@@ -161,7 +149,8 @@ public static String[] getPermissionsForRole(Start start, AppIdentifier appIdent
});
}
- public static String[] getRoles(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException {
+ public static String[] getRoles(Start start, AppIdentifier appIdentifier)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT role FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ?";
return execute(start, QUERY, pst -> pst.setString(1, appIdentifier.getAppId()), result -> {
ArrayList roles = new ArrayList<>();
@@ -235,9 +224,9 @@ public static boolean deleteRoleForUser_Transaction(Start start, Connection con,
return rowUpdatedCount > 0;
}
- public static boolean doesRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier, String role)
+ public static boolean doesRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier,
+ String role)
throws SQLException, StorageQueryException {
-
((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + role + Config.getConfig(start).getRolesTable());
String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable()
@@ -248,7 +237,8 @@ public static boolean doesRoleExist_transaction(Start start, Connection con, App
}, ResultSet::next);
}
- public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role) throws SQLException, StorageQueryException {
+ public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role)
+ throws SQLException, StorageQueryException {
String QUERY = "SELECT user_id FROM " + getConfig(start).getUserRolesTable()
+ " WHERE app_id = ? AND tenant_id = ? AND role = ? ";
return execute(start, QUERY, pst -> {
@@ -266,7 +256,8 @@ public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdent
public static boolean deletePermissionForRole_Transaction(Start start, Connection con, AppIdentifier appIdentifier,
String role,
- String permission) throws SQLException, StorageQueryException {
+ String permission)
+ throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getUserRolesPermissionsTable()
+ " WHERE app_id = ? AND role = ? AND permission = ? ";
@@ -314,7 +305,8 @@ public static String[] getRolesThatHavePermission(Start start, AppIdentifier app
});
}
- public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException {
+ public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId)
+ throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable()
+ " WHERE app_id = ? AND tenant_id = ? AND user_id = ?";
return update(start, QUERY, pst -> {
@@ -324,10 +316,12 @@ public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIden
});
}
- public static int deleteAllRolesForUser(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException {
+ public static int deleteAllRolesForUser_Transaction(Connection con, Start start,
+ AppIdentifier appIdentifier, String userId)
+ throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable()
+ " WHERE app_id = ? AND user_id = ?";
- return update(start, QUERY, pst -> {
+ return update(con, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, userId);
});
diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java
index 2140f984b..e97d3edf1 100644
--- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java
+++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java
@@ -28,15 +28,20 @@
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.exception.*;
import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException;
import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException;
import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage;
import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException;
import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException;
import io.supertokens.storageLayer.StorageLayer;
@@ -386,14 +391,135 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t
String userId)
throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException,
FeatureNotEnabledException, DuplicateEmailException, DuplicatePhoneNumberException,
- DuplicateThirdPartyUserException {
+ DuplicateThirdPartyUserException, AnotherPrimaryUserWithPhoneNumberAlreadyExistsException,
+ AnotherPrimaryUserWithEmailAlreadyExistsException,
+ AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException {
if (Arrays.stream(FeatureFlag.getInstance(main, new AppIdentifier(null, null)).getEnabledFeatures())
.noneMatch(ee_features -> ee_features == EE_FEATURES.MULTI_TENANCY)) {
throw new FeatureNotEnabledException(EE_FEATURES.MULTI_TENANCY);
}
- return tenantIdentifierWithStorage.getMultitenancyStorageWithTargetStorage()
- .addUserIdToTenant(tenantIdentifierWithStorage, userId);
+ AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage();
+ try {
+ return storage.startTransaction(con -> {
+ String tenantId = tenantIdentifierWithStorage.getTenantId();
+ AuthRecipeUserInfo userToAssociate = storage.getPrimaryUserById_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, userId);
+
+ if (userToAssociate != null && userToAssociate.isPrimaryUser) {
+ Set emails = new HashSet<>();
+ Set phoneNumbers = new HashSet<>();
+ Set thirdParties = new HashSet<>();
+
+ // Loop through all the emails, phoneNumbers and thirdPartyInfos and check for conflicts
+ for (LoginMethod lM : userToAssociate.loginMethods) {
+ if (lM.email != null) {
+ emails.add(lM.email);
+ }
+ if (lM.phoneNumber != null) {
+ phoneNumbers.add(lM.phoneNumber);
+ }
+ if (lM.thirdParty != null) {
+ thirdParties.add(lM.thirdParty);
+ }
+ }
+
+ for (String email : emails) {
+ AuthRecipeUserInfo[] usersWithSameEmail = storage.listPrimaryUsersByEmail_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, email);
+ for (AuthRecipeUserInfo userWithSameEmail : usersWithSameEmail) {
+ if (userWithSameEmail.getSupertokensUserId().equals(userToAssociate.getSupertokensUserId())) {
+ continue; // it's the same user, no need to check anything
+ }
+ if (userWithSameEmail.isPrimaryUser && userWithSameEmail.tenantIds.contains(tenantId) && !userWithSameEmail.getSupertokensUserId().equals(userId)) {
+ for (LoginMethod lm1 : userWithSameEmail.loginMethods) {
+ if (lm1.tenantIds.contains(tenantId)) {
+ for (LoginMethod lm2 : userToAssociate.loginMethods) {
+ if (lm1.recipeId.equals(lm2.recipeId) && email.equals(lm1.email) && lm1.email.equals(lm2.email)) {
+ throw new StorageTransactionLogicException(new DuplicateEmailException());
+ }
+ }
+ }
+ }
+ throw new StorageTransactionLogicException(new AnotherPrimaryUserWithEmailAlreadyExistsException(userWithSameEmail.getSupertokensUserId()));
+ }
+ }
+ }
+
+ for (String phoneNumber : phoneNumbers) {
+ AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage.listPrimaryUsersByPhoneNumber_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, phoneNumber);
+ for (AuthRecipeUserInfo userWithSamePhoneNumber : usersWithSamePhoneNumber) {
+ if (userWithSamePhoneNumber.getSupertokensUserId().equals(userToAssociate.getSupertokensUserId())) {
+ continue; // it's the same user, no need to check anything
+ }
+ if (userWithSamePhoneNumber.tenantIds.contains(tenantId) && !userWithSamePhoneNumber.getSupertokensUserId().equals(userId)) {
+ for (LoginMethod lm1 : userWithSamePhoneNumber.loginMethods) {
+ if (lm1.tenantIds.contains(tenantId)) {
+ for (LoginMethod lm2 : userToAssociate.loginMethods) {
+ if (lm1.recipeId.equals(lm2.recipeId) && phoneNumber.equals(lm1.phoneNumber) && lm1.phoneNumber.equals(lm2.phoneNumber)) {
+ throw new StorageTransactionLogicException(new DuplicatePhoneNumberException());
+ }
+ }
+ }
+ }
+ throw new StorageTransactionLogicException(new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(userWithSamePhoneNumber.getSupertokensUserId()));
+ }
+ }
+ }
+
+ for (LoginMethod.ThirdParty tp : thirdParties) {
+ AuthRecipeUserInfo[] usersWithSameThirdPartyInfo = storage.listPrimaryUsersByThirdPartyInfo_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, tp.id, tp.userId);
+ for (AuthRecipeUserInfo userWithSameThirdPartyInfo : usersWithSameThirdPartyInfo) {
+ if (userWithSameThirdPartyInfo.getSupertokensUserId().equals(userToAssociate.getSupertokensUserId())) {
+ continue; // it's the same user, no need to check anything
+ }
+ if (userWithSameThirdPartyInfo.tenantIds.contains(tenantId) && !userWithSameThirdPartyInfo.getSupertokensUserId().equals(userId)) {
+ for (LoginMethod lm1 : userWithSameThirdPartyInfo.loginMethods) {
+ if (lm1.tenantIds.contains(tenantId)) {
+ for (LoginMethod lm2 : userToAssociate.loginMethods) {
+ if (lm1.recipeId.equals(lm2.recipeId) && tp.equals(lm1.thirdParty) && lm1.thirdParty.equals(lm2.thirdParty)) {
+ throw new StorageTransactionLogicException(new DuplicateThirdPartyUserException());
+ }
+ }
+ }
+ }
+
+ throw new StorageTransactionLogicException(new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(userWithSameThirdPartyInfo.getSupertokensUserId()));
+ }
+ }
+ }
+ }
+
+ // userToAssociate may be null if the user is not associated to any tenants, we can still try and
+ // associate it. This happens only in CDI 3.0 where we allow disassociation from all tenants
+ // This will not happen in CDI >= 4.0 because we will not allow disassociation from all tenants
+ try {
+ boolean result = ((MultitenancySQLStorage) storage).addUserIdToTenant_Transaction(tenantIdentifierWithStorage, con, userId);
+ storage.commitTransaction(con);
+ return result;
+ } catch (TenantOrAppNotFoundException | UnknownUserIdException | DuplicatePhoneNumberException |
+ DuplicateThirdPartyUserException | DuplicateEmailException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof DuplicateEmailException) {
+ throw (DuplicateEmailException) e.actualException;
+ } else if (e.actualException instanceof DuplicatePhoneNumberException) {
+ throw (DuplicatePhoneNumberException) e.actualException;
+ } else if (e.actualException instanceof DuplicateThirdPartyUserException) {
+ throw (DuplicateThirdPartyUserException) e.actualException;
+ } else if (e.actualException instanceof TenantOrAppNotFoundException) {
+ throw (TenantOrAppNotFoundException) e.actualException;
+ } else if (e.actualException instanceof UnknownUserIdException) {
+ throw (UnknownUserIdException) e.actualException;
+ } else if (e.actualException instanceof AnotherPrimaryUserWithPhoneNumberAlreadyExistsException) {
+ throw (AnotherPrimaryUserWithPhoneNumberAlreadyExistsException) e.actualException;
+ } else if (e.actualException instanceof AnotherPrimaryUserWithEmailAlreadyExistsException) {
+ throw (AnotherPrimaryUserWithEmailAlreadyExistsException) e.actualException;
+ } else if (e.actualException instanceof AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException) {
+ throw (AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException) e.actualException;
+ }
+ throw new StorageQueryException(e.actualException);
+ }
}
public static boolean removeUserIdFromTenant(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage,
diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java
new file mode 100644
index 000000000..c95bfdcc3
--- /dev/null
+++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java
@@ -0,0 +1,23 @@
+/*
+ * 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 io.supertokens.multitenancy.exception;
+
+public class AnotherPrimaryUserWithEmailAlreadyExistsException extends Exception {
+ public AnotherPrimaryUserWithEmailAlreadyExistsException(String primaryUserId) {
+ super("Another primary user with email already exists: " + primaryUserId);
+ }
+}
diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java
new file mode 100644
index 000000000..e012f9349
--- /dev/null
+++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java
@@ -0,0 +1,23 @@
+/*
+ * 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 io.supertokens.multitenancy.exception;
+
+public class AnotherPrimaryUserWithPhoneNumberAlreadyExistsException extends Exception {
+ public AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(String primaryUserId) {
+ super("Another primary user with phone number already exists: " + primaryUserId);
+ }
+}
diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java
new file mode 100644
index 000000000..d5f413cf5
--- /dev/null
+++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java
@@ -0,0 +1,23 @@
+/*
+ * 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 io.supertokens.multitenancy.exception;
+
+public class AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException extends Exception {
+ public AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(String primaryUserId) {
+ super("Another primary user with third party info already exists: " + primaryUserId);
+ }
+}
diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java
index 29a38cd02..798d0b6ae 100644
--- a/src/main/java/io/supertokens/passwordless/Passwordless.java
+++ b/src/main/java/io/supertokens/passwordless/Passwordless.java
@@ -17,11 +17,17 @@
package io.supertokens.passwordless;
import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.config.Config;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.passwordless.exceptions.*;
+import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.Storage;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
@@ -33,7 +39,6 @@
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
-import io.supertokens.pluginInterface.passwordless.UserInfo;
import io.supertokens.pluginInterface.passwordless.exception.*;
import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage;
import io.supertokens.storageLayer.StorageLayer;
@@ -46,6 +51,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@@ -72,7 +78,8 @@ public static CreateCodeResponse createCode(Main main, String email, String phon
}
}
- public static CreateCodeResponse createCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String email,
+ public static CreateCodeResponse createCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
+ String email,
String phoneNumber, @Nullable String deviceId,
@Nullable String userInputCode)
throws RestartFlowException, DuplicateLinkCodeHashException,
@@ -190,7 +197,8 @@ public static List getDevicesWithCodesByEmail(
PasswordlessDevice[] devices = passwordlessStorage.getDevicesByEmail(tenantIdentifierWithStorage, email);
ArrayList result = new ArrayList();
for (PasswordlessDevice device : devices) {
- PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage, device.deviceIdHash);
+ PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage,
+ device.deviceIdHash);
result.add(new DeviceWithCodes(device, codes));
}
@@ -210,10 +218,12 @@ public static List getDevicesWithCodesByPhoneNumber(
throws StorageQueryException {
PasswordlessSQLStorage passwordlessStorage = tenantIdentifierWithStorage.getPasswordlessStorage();
- PasswordlessDevice[] devices = passwordlessStorage.getDevicesByPhoneNumber(tenantIdentifierWithStorage, phoneNumber);
+ PasswordlessDevice[] devices = passwordlessStorage.getDevicesByPhoneNumber(tenantIdentifierWithStorage,
+ phoneNumber);
ArrayList result = new ArrayList();
for (PasswordlessDevice device : devices) {
- PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage, device.deviceIdHash);
+ PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage,
+ device.deviceIdHash);
result.add(new DeviceWithCodes(device, codes));
}
@@ -240,12 +250,13 @@ public static ConsumeCodeResponse consumeCode(Main main,
Storage storage = StorageLayer.getStorage(main);
return consumeCode(
new TenantIdentifierWithStorage(null, null, null, storage),
- main, deviceId, deviceIdHashFromUser, userInputCode, linkCode);
+ main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
}
}
+ @TestOnly
public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
String deviceId, String deviceIdHashFromUser,
String userInputCode, String linkCode)
@@ -253,6 +264,17 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException,
StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException,
TenantOrAppNotFoundException, BadPermissionException {
+ return consumeCode(tenantIdentifierWithStorage, main, deviceId, deviceIdHashFromUser, userInputCode, linkCode,
+ false);
+ }
+
+ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
+ String deviceId, String deviceIdHashFromUser,
+ String userInputCode, String linkCode, boolean setEmailVerified)
+ throws RestartFlowException, ExpiredUserInputCodeException,
+ IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException,
+ StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException,
+ TenantOrAppNotFoundException, BadPermissionException {
TenantConfig config = Multitenancy.getTenantInfo(main, tenantIdentifierWithStorage);
if (config == null) {
@@ -274,7 +296,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
PasswordlessLinkCode parsedCode = PasswordlessLinkCode.decodeString(linkCode);
linkCodeHash = parsedCode.getHash();
- PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash(tenantIdentifierWithStorage, linkCodeHash.encode());
+ PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash(tenantIdentifierWithStorage,
+ linkCodeHash.encode());
if (code == null || code.createdAt < (System.currentTimeMillis() - passwordlessCodeLifetime)) {
throw new RestartFlowException();
}
@@ -284,7 +307,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
PasswordlessDeviceId parsedDeviceId = PasswordlessDeviceId.decodeString(deviceId);
deviceIdHash = parsedDeviceId.getHash();
- PasswordlessDevice device = passwordlessStorage.getDevice(tenantIdentifierWithStorage, deviceIdHash.encode());
+ PasswordlessDevice device = passwordlessStorage.getDevice(tenantIdentifierWithStorage,
+ deviceIdHash.encode());
if (device == null) {
throw new RestartFlowException();
}
@@ -306,12 +330,14 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
}
if (device.failedAttempts >= maxCodeInputAttempts) {
// This can happen if the configured maxCodeInputAttempts changes
- passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con, deviceIdHash.encode());
+ passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con,
+ deviceIdHash.encode());
passwordlessStorage.commitTransaction(con);
throw new StorageTransactionLogicException(new RestartFlowException());
}
- PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash_Transaction(tenantIdentifierWithStorage, con,
+ PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash_Transaction(
+ tenantIdentifierWithStorage, con,
linkCodeHash.encode());
if (code == null || code.createdAt < System.currentTimeMillis() - passwordlessCodeLifetime) {
if (deviceId != null) {
@@ -319,11 +345,13 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
// the code expired. This means that we need to increment failedAttempts or clean up the device
// if it would exceed the configured max.
if (device.failedAttempts + 1 >= maxCodeInputAttempts) {
- passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con, deviceIdHash.encode());
+ passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con,
+ deviceIdHash.encode());
passwordlessStorage.commitTransaction(con);
throw new StorageTransactionLogicException(new RestartFlowException());
} else {
- passwordlessStorage.incrementDeviceFailedAttemptCount_Transaction(tenantIdentifierWithStorage, con,
+ passwordlessStorage.incrementDeviceFailedAttemptCount_Transaction(
+ tenantIdentifierWithStorage, con,
deviceIdHash.encode());
passwordlessStorage.commitTransaction(con);
@@ -340,7 +368,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
}
if (device.email != null) {
- passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifierWithStorage, con, device.email);
+ passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifierWithStorage, con,
+ device.email);
} else if (device.phoneNumber != null) {
passwordlessStorage.deleteDevicesByPhoneNumber_Transaction(tenantIdentifierWithStorage, con,
device.phoneNumber);
@@ -363,9 +392,35 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
}
// Getting here means that we successfully consumed the code
- UserInfo user = consumedDevice.email != null ?
- passwordlessStorage.getUserByEmail(tenantIdentifierWithStorage, consumedDevice.email)
- : passwordlessStorage.getUserByPhoneNumber(tenantIdentifierWithStorage, consumedDevice.phoneNumber);
+ AuthRecipeUserInfo user = null;
+ LoginMethod loginMethod = null;
+ if (consumedDevice.email != null) {
+ AuthRecipeUserInfo[] users = passwordlessStorage.listPrimaryUsersByEmail(tenantIdentifierWithStorage,
+ consumedDevice.email);
+ for (AuthRecipeUserInfo currUser : users) {
+ for (LoginMethod currLM : currUser.loginMethods) {
+ if (currLM.recipeId == RECIPE_ID.PASSWORDLESS && currLM.email.equals(consumedDevice.email) && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) {
+ user = currUser;
+ loginMethod = currLM;
+ break;
+ }
+ }
+ }
+ } else {
+ AuthRecipeUserInfo[] users = passwordlessStorage.listPrimaryUsersByPhoneNumber(tenantIdentifierWithStorage,
+ consumedDevice.phoneNumber);
+ for (AuthRecipeUserInfo currUser : users) {
+ for (LoginMethod currLM : currUser.loginMethods) {
+ if (currLM.recipeId == RECIPE_ID.PASSWORDLESS &&
+ currLM.phoneNumber.equals(consumedDevice.phoneNumber) && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) {
+ user = currUser;
+ loginMethod = currLM;
+ break;
+ }
+ }
+ }
+ }
+
if (user == null) {
while (true) {
try {
@@ -373,7 +428,34 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
long timeJoined = System.currentTimeMillis();
user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email,
consumedDevice.phoneNumber, timeJoined);
- return new ConsumeCodeResponse(true, user);
+
+ // Set email as verified, if using email
+ if (setEmailVerified && consumedDevice.email != null) {
+ try {
+ AuthRecipeUserInfo finalUser = user;
+ tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> {
+ try {
+ tenantIdentifierWithStorage.getEmailVerificationStorage()
+ .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con,
+ finalUser.getSupertokensUserId(), consumedDevice.email, true);
+ tenantIdentifierWithStorage.getEmailVerificationStorage()
+ .commitTransaction(con);
+
+ return null;
+ } catch (TenantOrAppNotFoundException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ user.loginMethods[0].setVerified(); // newly created user has only one loginMethod
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof TenantOrAppNotFoundException) {
+ throw (TenantOrAppNotFoundException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
+
+ return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber);
} catch (DuplicateEmailException | DuplicatePhoneNumberException e) {
// Getting these would mean that between getting the user and trying creating it:
// 1. the user managed to do a full create+consume flow
@@ -389,14 +471,40 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant
} else {
// We do not need this cleanup if we are creating the user, since it uses the email/phoneNumber of the
// device, which has already been cleaned up
- if (user.email != null && !user.email.equals(consumedDevice.email)) {
- removeCodesByEmail(tenantIdentifierWithStorage, user.email);
+ if (setEmailVerified && consumedDevice.email != null) {
+ // Set email verification
+ try {
+ LoginMethod finalLoginMethod = loginMethod;
+ tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> {
+ try {
+ tenantIdentifierWithStorage.getEmailVerificationStorage()
+ .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con,
+ finalLoginMethod.getSupertokensUserId(), consumedDevice.email, true);
+ tenantIdentifierWithStorage.getEmailVerificationStorage()
+ .commitTransaction(con);
+
+ return null;
+ } catch (TenantOrAppNotFoundException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ loginMethod.setVerified();
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof TenantOrAppNotFoundException) {
+ throw (TenantOrAppNotFoundException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
+
+ if (loginMethod.email != null && !loginMethod.email.equals(consumedDevice.email)) {
+ removeCodesByEmail(tenantIdentifierWithStorage, loginMethod.email);
}
- if (user.phoneNumber != null && !user.phoneNumber.equals(consumedDevice.phoneNumber)) {
- removeCodesByPhoneNumber(tenantIdentifierWithStorage, user.phoneNumber);
+ if (loginMethod.phoneNumber != null && !loginMethod.phoneNumber.equals(consumedDevice.phoneNumber)) {
+ removeCodesByPhoneNumber(tenantIdentifierWithStorage, loginMethod.phoneNumber);
}
}
- return new ConsumeCodeResponse(false, user);
+ return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber);
}
@TestOnly
@@ -421,7 +529,8 @@ public static void removeCode(TenantIdentifierWithStorage tenantIdentifierWithSt
// Locking the device
passwordlessStorage.getDevice_Transaction(tenantIdentifierWithStorage, con, code.deviceIdHash);
- PasswordlessCode[] allCodes = passwordlessStorage.getCodesOfDevice_Transaction(tenantIdentifierWithStorage, con,
+ PasswordlessCode[] allCodes = passwordlessStorage.getCodesOfDevice_Transaction(tenantIdentifierWithStorage,
+ con,
code.deviceIdHash);
if (!Stream.of(allCodes).anyMatch(code::equals)) {
// Already deleted
@@ -482,107 +591,189 @@ public static void removeCodesByPhoneNumber(TenantIdentifierWithStorage tenantId
}
@TestOnly
- public static UserInfo getUserById(Main main, String userId)
+ @Deprecated
+ public static AuthRecipeUserInfo getUserById(Main main, String userId)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUserById(
new AppIdentifierWithStorage(null, null, storage), userId);
}
- public static UserInfo getUserById(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
+ @Deprecated
+ public static AuthRecipeUserInfo getUserById(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
throws StorageQueryException {
- return appIdentifierWithStorage.getPasswordlessStorage()
- .getUserById(appIdentifierWithStorage, userId);
+ AuthRecipeUserInfo result = appIdentifierWithStorage.getAuthRecipeStorage()
+ .getPrimaryUserById(appIdentifierWithStorage, userId);
+ if (result == null) {
+ return null;
+ }
+ for (LoginMethod lM : result.loginMethods) {
+ if (lM.getSupertokensUserId().equals(userId) && lM.recipeId == RECIPE_ID.PASSWORDLESS) {
+ return AuthRecipeUserInfo.create(lM.getSupertokensUserId(), result.isPrimaryUser,
+ lM);
+ }
+ }
+ return null;
}
@TestOnly
- public static UserInfo getUserByPhoneNumber(Main main,
- String phoneNumber) throws StorageQueryException {
+ public static AuthRecipeUserInfo getUserByPhoneNumber(Main main,
+ String phoneNumber) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUserByPhoneNumber(
new TenantIdentifierWithStorage(null, null, null, storage),
phoneNumber);
}
- public static UserInfo getUserByPhoneNumber(TenantIdentifierWithStorage tenantIdentifierWithStorage,
- String phoneNumber) throws StorageQueryException {
- return tenantIdentifierWithStorage.getPasswordlessStorage()
- .getUserByPhoneNumber(tenantIdentifierWithStorage, phoneNumber);
+ @Deprecated
+ public static AuthRecipeUserInfo getUserByPhoneNumber(TenantIdentifierWithStorage tenantIdentifierWithStorage,
+ String phoneNumber) throws StorageQueryException {
+ AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getPasswordlessStorage()
+ .listPrimaryUsersByPhoneNumber(tenantIdentifierWithStorage, phoneNumber);
+ for (AuthRecipeUserInfo user : users) {
+ for (LoginMethod lM : user.loginMethods) {
+ if (lM.recipeId == RECIPE_ID.PASSWORDLESS && lM.phoneNumber.equals(phoneNumber)) {
+ return user;
+ }
+ }
+ }
+ return null;
}
+ @Deprecated
@TestOnly
- public static UserInfo getUserByEmail(Main main, String email)
+ public static AuthRecipeUserInfo getUserByEmail(Main main, String email)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUserByEmail(
- new TenantIdentifierWithStorage(null, null, null,storage), email);
+ new TenantIdentifierWithStorage(null, null, null, storage), email);
}
- public static UserInfo getUserByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, String email)
+ @Deprecated
+ public static AuthRecipeUserInfo getUserByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage,
+ String email)
throws StorageQueryException {
- return tenantIdentifierWithStorage.getPasswordlessStorage().getUserByEmail(tenantIdentifierWithStorage, email);
+ AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getPasswordlessStorage()
+ .listPrimaryUsersByEmail(tenantIdentifierWithStorage, email);
+ for (AuthRecipeUserInfo user : users) {
+ for (LoginMethod lM : user.loginMethods) {
+ if (lM.recipeId == RECIPE_ID.PASSWORDLESS && lM.email.equals(email)) {
+ return user;
+ }
+ }
+ }
+ return null;
}
@TestOnly
public static void updateUser(Main main, String userId,
FieldUpdate emailUpdate, FieldUpdate phoneNumberUpdate)
throws StorageQueryException, UnknownUserIdException, DuplicateEmailException,
- DuplicatePhoneNumberException, UserWithoutContactInfoException {
+ DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException,
+ PhoneNumberChangeNotAllowedException {
Storage storage = StorageLayer.getStorage(main);
updateUser(new AppIdentifierWithStorage(null, null, storage),
userId, emailUpdate, phoneNumberUpdate);
}
- public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId,
+ public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, String recipeUserId,
FieldUpdate emailUpdate, FieldUpdate phoneNumberUpdate)
throws StorageQueryException, UnknownUserIdException, DuplicateEmailException,
- DuplicatePhoneNumberException, UserWithoutContactInfoException {
+ DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException,
+ PhoneNumberChangeNotAllowedException {
PasswordlessSQLStorage storage = appIdentifierWithStorage.getPasswordlessStorage();
// We do not lock the user here, because we decided that even if the device cleanup used outdated information
// it wouldn't leave the system in an incosistent state/cause problems.
- UserInfo user = storage.getUserById(appIdentifierWithStorage, userId);
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(appIdentifierWithStorage, recipeUserId);
if (user == null) {
throw new UnknownUserIdException();
}
- boolean emailWillBeDefined = emailUpdate != null ? emailUpdate.newValue != null : user.email != null;
+
+ LoginMethod lM = Arrays.stream(user.loginMethods)
+ .filter(currlM -> currlM.getSupertokensUserId().equals(recipeUserId) && currlM.recipeId == RECIPE_ID.PASSWORDLESS)
+ .findFirst().orElse(null);
+
+ if (lM == null) {
+ throw new UnknownUserIdException();
+ }
+
+ boolean emailWillBeDefined = emailUpdate != null ? emailUpdate.newValue != null : lM.email != null;
boolean phoneNumberWillBeDefined = phoneNumberUpdate != null ? phoneNumberUpdate.newValue != null
- : user.phoneNumber != null;
+ : lM.phoneNumber != null;
if (!emailWillBeDefined && !phoneNumberWillBeDefined) {
throw new UserWithoutContactInfoException();
}
try {
+ AuthRecipeSQLStorage authRecipeSQLStorage =
+ (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage();
storage.startTransaction(con -> {
- if (emailUpdate != null && !Objects.equals(emailUpdate.newValue, user.email)) {
+ if (emailUpdate != null && !Objects.equals(emailUpdate.newValue, lM.email)) {
+ if (user.isPrimaryUser) {
+ for (String tenantId : user.tenantIds) {
+ AuthRecipeUserInfo[] existingUsersWithNewEmail =
+ authRecipeSQLStorage.listPrimaryUsersByEmail_Transaction(
+ appIdentifierWithStorage, con,
+ emailUpdate.newValue);
+
+ for (AuthRecipeUserInfo userWithSameEmail : existingUsersWithNewEmail) {
+ if (!userWithSameEmail.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(user.getSupertokensUserId())) {
+ throw new StorageTransactionLogicException(
+ new EmailChangeNotAllowedException());
+ }
+ }
+ }
+ }
try {
- storage.updateUserEmail_Transaction(appIdentifierWithStorage, con, userId,
+ storage.updateUserEmail_Transaction(appIdentifierWithStorage, con, recipeUserId,
emailUpdate.newValue);
} catch (UnknownUserIdException | DuplicateEmailException e) {
throw new StorageTransactionLogicException(e);
}
- if (user.email != null) {
- storage.deleteDevicesByEmail_Transaction(appIdentifierWithStorage, con, user.email,
- userId);
+ if (lM.email != null) {
+ storage.deleteDevicesByEmail_Transaction(appIdentifierWithStorage, con, lM.email,
+ recipeUserId);
}
if (emailUpdate.newValue != null) {
storage.deleteDevicesByEmail_Transaction(appIdentifierWithStorage, con,
- emailUpdate.newValue, userId);
+ emailUpdate.newValue, recipeUserId);
}
}
- if (phoneNumberUpdate != null && !Objects.equals(phoneNumberUpdate.newValue, user.phoneNumber)) {
+ if (phoneNumberUpdate != null && !Objects.equals(phoneNumberUpdate.newValue, lM.phoneNumber)) {
+ if (user.isPrimaryUser) {
+ for (String tenantId : user.tenantIds) {
+ AuthRecipeUserInfo[] existingUsersWithNewPhoneNumber =
+ authRecipeSQLStorage.listPrimaryUsersByPhoneNumber_Transaction(
+ appIdentifierWithStorage, con,
+ phoneNumberUpdate.newValue);
+
+ for (AuthRecipeUserInfo userWithSamePhoneNumber : existingUsersWithNewPhoneNumber) {
+ if (!userWithSamePhoneNumber.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (userWithSamePhoneNumber.isPrimaryUser && !userWithSamePhoneNumber.getSupertokensUserId().equals(user.getSupertokensUserId())) {
+ throw new StorageTransactionLogicException(
+ new PhoneNumberChangeNotAllowedException());
+ }
+ }
+ }
+ }
try {
- storage.updateUserPhoneNumber_Transaction(appIdentifierWithStorage, con, userId,
+ storage.updateUserPhoneNumber_Transaction(appIdentifierWithStorage, con, recipeUserId,
phoneNumberUpdate.newValue);
} catch (UnknownUserIdException | DuplicatePhoneNumberException e) {
throw new StorageTransactionLogicException(e);
}
- if (user.phoneNumber != null) {
+ if (lM.phoneNumber != null) {
storage.deleteDevicesByPhoneNumber_Transaction(appIdentifierWithStorage, con,
- user.phoneNumber, userId);
+ lM.phoneNumber, recipeUserId);
}
if (phoneNumberUpdate.newValue != null) {
storage.deleteDevicesByPhoneNumber_Transaction(appIdentifierWithStorage, con,
- phoneNumberUpdate.newValue, userId);
+ phoneNumberUpdate.newValue, recipeUserId);
}
}
storage.commitTransaction(con);
@@ -600,6 +791,14 @@ public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage,
if (e.actualException instanceof DuplicatePhoneNumberException) {
throw (DuplicatePhoneNumberException) e.actualException;
}
+
+ if (e.actualException instanceof EmailChangeNotAllowedException) {
+ throw (EmailChangeNotAllowedException) e.actualException;
+ }
+
+ if (e.actualException instanceof PhoneNumberChangeNotAllowedException) {
+ throw (PhoneNumberChangeNotAllowedException) e.actualException;
+ }
}
}
@@ -646,11 +845,15 @@ public CreateCodeResponse(String deviceIdHash, String codeId, String deviceId, S
public static class ConsumeCodeResponse {
public boolean createdNewUser;
- public UserInfo user;
+ public AuthRecipeUserInfo user;
+ public String email;
+ public String phoneNumber;
- public ConsumeCodeResponse(boolean createdNewUser, UserInfo user) {
+ public ConsumeCodeResponse(boolean createdNewUser, AuthRecipeUserInfo user, String email, String phoneNumber) {
this.createdNewUser = createdNewUser;
this.user = user;
+ this.email = email;
+ this.phoneNumber = phoneNumber;
}
}
diff --git a/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java b/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java
new file mode 100644
index 000000000..d0117d1cc
--- /dev/null
+++ b/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java
@@ -0,0 +1,20 @@
+/*
+ * 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 io.supertokens.passwordless.exceptions;
+
+public class PhoneNumberChangeNotAllowedException extends Exception {
+}
diff --git a/src/main/java/io/supertokens/session/Session.java b/src/main/java/io/supertokens/session/Session.java
index 51771bb21..dcbbdd204 100644
--- a/src/main/java/io/supertokens/session/Session.java
+++ b/src/main/java/io/supertokens/session/Session.java
@@ -29,6 +29,8 @@
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.Storage;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.*;
@@ -64,7 +66,7 @@ public class Session {
@TestOnly
public static SessionInformationHolder createNewSession(TenantIdentifierWithStorage tenantIdentifierWithStorage,
Main main,
- @Nonnull String userId,
+ @Nonnull String recipeUserId,
@Nonnull JsonObject userDataInJWT,
@Nonnull JsonObject userDataInDatabase)
throws NoSuchAlgorithmException, StorageQueryException, InvalidKeyException,
@@ -72,7 +74,8 @@ public static SessionInformationHolder createNewSession(TenantIdentifierWithStor
BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException, UnauthorisedException,
JWT.JWTException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
- return createNewSession(tenantIdentifierWithStorage, main, userId, userDataInJWT, userDataInDatabase, false,
+ return createNewSession(tenantIdentifierWithStorage, main, recipeUserId, userDataInJWT, userDataInDatabase,
+ false,
AccessToken.getLatestVersion(), false);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@@ -81,7 +84,7 @@ public static SessionInformationHolder createNewSession(TenantIdentifierWithStor
@TestOnly
public static SessionInformationHolder createNewSession(Main main,
- @Nonnull String userId,
+ @Nonnull String recipeUserId,
@Nonnull JsonObject userDataInJWT,
@Nonnull JsonObject userDataInDatabase)
throws NoSuchAlgorithmException, StorageQueryException, InvalidKeyException,
@@ -92,14 +95,14 @@ public static SessionInformationHolder createNewSession(Main main,
try {
return createNewSession(
new TenantIdentifierWithStorage(null, null, null, storage), main,
- userId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false);
+ recipeUserId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
}
@TestOnly
- public static SessionInformationHolder createNewSession(Main main, @Nonnull String userId,
+ public static SessionInformationHolder createNewSession(Main main, @Nonnull String recipeUserId,
@Nonnull JsonObject userDataInJWT,
@Nonnull JsonObject userDataInDatabase,
boolean enableAntiCsrf, AccessToken.VERSION version,
@@ -112,14 +115,14 @@ public static SessionInformationHolder createNewSession(Main main, @Nonnull Stri
try {
return createNewSession(
new TenantIdentifierWithStorage(null, null, null, storage), main,
- userId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey);
+ recipeUserId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
}
public static SessionInformationHolder createNewSession(TenantIdentifierWithStorage tenantIdentifierWithStorage,
- Main main, @Nonnull String userId,
+ Main main, @Nonnull String recipeUserId,
@Nonnull JsonObject userDataInJWT,
@Nonnull JsonObject userDataInDatabase,
boolean enableAntiCsrf, AccessToken.VERSION version,
@@ -133,23 +136,35 @@ public static SessionInformationHolder createNewSession(TenantIdentifierWithStor
sessionHandle += "_" + tenantIdentifierWithStorage.getTenantId();
}
+ String primaryUserId = recipeUserId;
+ if (tenantIdentifierWithStorage.getStorage().getType().equals(STORAGE_TYPE.SQL)) {
+ tenantIdentifierWithStorage.getAuthRecipeStorage()
+ .getPrimaryUserIdStrForUserId(tenantIdentifierWithStorage.toAppIdentifier(), recipeUserId);
+ if (primaryUserId == null) {
+ primaryUserId = recipeUserId;
+ }
+ }
+
String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null;
final TokenInfo refreshToken = RefreshToken.createNewRefreshToken(tenantIdentifierWithStorage, main,
- sessionHandle, userId, null,
+ sessionHandle, recipeUserId, null,
antiCsrfToken);
TokenInfo accessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, sessionHandle,
- userId, Utils.hashSHA256(refreshToken.token), null, userDataInJWT, antiCsrfToken,
+ recipeUserId, primaryUserId, Utils.hashSHA256(refreshToken.token), null, userDataInJWT, antiCsrfToken,
null, version, useStaticKey);
tenantIdentifierWithStorage.getSessionStorage()
- .createNewSession(tenantIdentifierWithStorage, sessionHandle, userId,
+ .createNewSession(tenantIdentifierWithStorage, sessionHandle, recipeUserId,
Utils.hashSHA256(Utils.hashSHA256(refreshToken.token)), userDataInDatabase, refreshToken.expiry,
userDataInJWT, refreshToken.createdTime, useStaticKey);
TokenInfo idRefreshToken = new TokenInfo(UUID.randomUUID().toString(), refreshToken.expiry,
refreshToken.createdTime);
- return new SessionInformationHolder(new SessionInfo(sessionHandle, userId, userDataInJWT, tenantIdentifierWithStorage.getTenantId()), accessToken,
+ return new SessionInformationHolder(
+ new SessionInfo(sessionHandle, primaryUserId, recipeUserId, userDataInJWT,
+ tenantIdentifierWithStorage.getTenantId()),
+ accessToken,
refreshToken, idRefreshToken, antiCsrfToken);
}
@@ -201,7 +216,8 @@ public static SessionInformationHolder regenerateToken(AppIdentifier appIdentifi
accessToken.sessionHandle);
JsonObject newJWTUserPayload = userDataInJWT == null ? sessionInfo.userDataInJWT
: userDataInJWT;
- updateSession(tenantIdentifierWithStorage, accessToken.sessionHandle, null, newJWTUserPayload, accessToken.version);
+ updateSession(tenantIdentifierWithStorage, accessToken.sessionHandle, null, newJWTUserPayload,
+ accessToken.version);
// if the above succeeds but the below fails, it's OK since the client will get server error and will try
// again. In this case, the JWT data will be updated again since the API will get the old JWT. In case there
@@ -210,17 +226,21 @@ public static SessionInformationHolder regenerateToken(AppIdentifier appIdentifi
// in this case, we set the should not set the access token in the response since they will have to call
// the refresh API anyway.
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()), null, null, null,
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId,
+ newJWTUserPayload,
+ tenantIdentifierWithStorage.getTenantId()), null, null, null,
null);
}
TokenInfo newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main,
- accessToken.sessionHandle, accessToken.userId,
+ accessToken.sessionHandle, accessToken.recipeUserId, accessToken.primaryUserId,
accessToken.refreshTokenHash1, accessToken.parentRefreshTokenHash1, newJWTUserPayload,
accessToken.antiCsrfToken, accessToken.expiryTime, accessToken.version, sessionInfo.useStaticKey);
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()),
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId,
+ newJWTUserPayload,
+ tenantIdentifierWithStorage.getTenantId()),
new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime), null, null,
null);
}
@@ -255,18 +275,22 @@ public static SessionInformationHolder regenerateTokenBeforeCDI2_21(AppIdentifie
// in this case, we set the should not set the access token in the response since they will have to call
// the refresh API anyway.
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()), null, null, null,
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId,
+ newJWTUserPayload,
+ tenantIdentifierWithStorage.getTenantId()), null, null, null,
null);
}
TokenInfo newAccessToken = AccessToken.createNewAccessToken(accessToken.tenantIdentifier, main,
accessToken.sessionHandle,
- accessToken.userId,
+ accessToken.recipeUserId, accessToken.primaryUserId,
accessToken.refreshTokenHash1, accessToken.parentRefreshTokenHash1, newJWTUserPayload,
accessToken.antiCsrfToken, accessToken.expiryTime, accessToken.version, sessionInfo.useStaticKey);
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()),
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId,
+ newJWTUserPayload,
+ tenantIdentifierWithStorage.getTenantId()),
new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime), null, null,
null);
}
@@ -319,7 +343,9 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M
// this means that the refresh token associated with this access token is
// already the parent - and JWT payload doesn't need to be updated.
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, accessToken.userData, tenantIdentifierWithStorage.getTenantId()), null, null,
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId,
+ accessToken.userData,
+ tenantIdentifierWithStorage.getTenantId()), null, null,
null, null);
}
@@ -360,18 +386,20 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M
newAccessToken = AccessToken.createNewAccessTokenV1(tenantIdentifierWithStorage,
main,
accessToken.sessionHandle,
- accessToken.userId, accessToken.refreshTokenHash1, null,
+ accessToken.recipeUserId, accessToken.refreshTokenHash1, null,
sessionInfo.userDataInJWT, accessToken.antiCsrfToken);
} else {
newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main,
accessToken.sessionHandle,
- accessToken.userId, accessToken.refreshTokenHash1, null,
+ accessToken.recipeUserId, accessToken.primaryUserId,
+ accessToken.refreshTokenHash1, null,
sessionInfo.userDataInJWT, accessToken.antiCsrfToken, null, accessToken.version,
sessionInfo.useStaticKey);
}
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId,
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId,
+ accessToken.recipeUserId,
sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()),
new TokenInfo(newAccessToken.token, newAccessToken.expiry,
newAccessToken.createdTime),
@@ -380,13 +408,16 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M
storage.commitTransaction(con);
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, accessToken.userData, tenantIdentifierWithStorage.getTenantId()),
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId,
+ accessToken.recipeUserId, accessToken.userData,
+ tenantIdentifierWithStorage.getTenantId()),
// here we purposely use accessToken.userData instead of sessionInfo.userDataInJWT
// because we are not returning a new access token
null, null, null, null);
} catch (UnauthorisedException | NoSuchAlgorithmException |
- InvalidKeyException | InvalidKeySpecException | SignatureException |
- UnsupportedJWTSigningAlgorithmException | AccessTokenPayloadError | TenantOrAppNotFoundException e) {
+ InvalidKeyException | InvalidKeySpecException | SignatureException |
+ UnsupportedJWTSigningAlgorithmException | AccessTokenPayloadError |
+ TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
}, SQLStorage.TransactionIsolationLevel.REPEATABLE_READ);
@@ -433,29 +464,34 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M
if (accessToken.version == AccessToken.VERSION.V1) {
newAccessToken = AccessToken.createNewAccessTokenV1(tenantIdentifierWithStorage, main,
accessToken.sessionHandle,
- accessToken.userId, accessToken.refreshTokenHash1, null, sessionInfo.userDataInJWT,
+ accessToken.recipeUserId, accessToken.refreshTokenHash1, null,
+ sessionInfo.userDataInJWT,
accessToken.antiCsrfToken);
} else {
newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main,
accessToken.sessionHandle,
- accessToken.userId, accessToken.refreshTokenHash1, null, sessionInfo.userDataInJWT,
+ accessToken.recipeUserId, accessToken.primaryUserId, accessToken.refreshTokenHash1,
+ null, sessionInfo.userDataInJWT,
accessToken.antiCsrfToken, null, accessToken.version, sessionInfo.useStaticKey);
}
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId,
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId,
+ accessToken.recipeUserId,
sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()),
new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime),
null, null, null);
}
return new SessionInformationHolder(
- new SessionInfo(accessToken.sessionHandle, accessToken.userId, accessToken.userData, tenantIdentifierWithStorage.getTenantId()),
+ new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId,
+ accessToken.recipeUserId, accessToken.userData,
+ tenantIdentifierWithStorage.getTenantId()),
// here we purposely use accessToken.userData instead of sessionInfo.userDataInJWT
// because we are not returning a new access token
null, null, null, null);
} catch (NoSuchAlgorithmException | InvalidKeyException
- | InvalidKeySpecException | SignatureException e) {
+ | InvalidKeySpecException | SignatureException e) {
throw new StorageTransactionLogicException(e);
}
}
@@ -533,14 +569,16 @@ private static SessionInformationHolder refreshSessionHelper(
if (sessionInfo.refreshTokenHash2.equals(Utils.hashSHA256(Utils.hashSHA256(refreshToken)))) {
// at this point, the input refresh token is the parent one.
storage.commitTransaction(con);
+
String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null;
final TokenInfo newRefreshToken = RefreshToken.createNewRefreshToken(
tenantIdentifierWithStorage, main, sessionHandle,
- sessionInfo.userId, Utils.hashSHA256(refreshToken), antiCsrfToken);
+ sessionInfo.recipeUserId, Utils.hashSHA256(refreshToken), antiCsrfToken);
TokenInfo newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage,
main, sessionHandle,
- sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token),
+ sessionInfo.recipeUserId, sessionInfo.userId,
+ Utils.hashSHA256(newRefreshToken.token),
Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken,
null, accessTokenVersion, sessionInfo.useStaticKey);
@@ -548,7 +586,9 @@ private static SessionInformationHolder refreshSessionHelper(
newRefreshToken.expiry, newRefreshToken.createdTime);
return new SessionInformationHolder(
- new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()),
+ new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.recipeUserId,
+ sessionInfo.userDataInJWT,
+ tenantIdentifierWithStorage.getTenantId()),
newAccessToken, newRefreshToken, idRefreshToken, antiCsrfToken);
}
@@ -571,13 +611,15 @@ private static SessionInformationHolder refreshSessionHelper(
storage.commitTransaction(con);
- throw new TokenTheftDetectedException(sessionHandle, sessionInfo.userId);
+ throw new TokenTheftDetectedException(sessionHandle, sessionInfo.recipeUserId,
+ sessionInfo.userId);
} catch (UnauthorisedException | NoSuchAlgorithmException | InvalidKeyException
- | AccessTokenPayloadError | TokenTheftDetectedException | InvalidKeySpecException
- | SignatureException | NoSuchPaddingException | InvalidAlgorithmParameterException
- | IllegalBlockSizeException | BadPaddingException | UnsupportedJWTSigningAlgorithmException |
- TenantOrAppNotFoundException e) {
+ | AccessTokenPayloadError | TokenTheftDetectedException | InvalidKeySpecException
+ | SignatureException | NoSuchPaddingException | InvalidAlgorithmParameterException
+ | IllegalBlockSizeException | BadPaddingException |
+ UnsupportedJWTSigningAlgorithmException |
+ TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
});
@@ -619,10 +661,10 @@ private static SessionInformationHolder refreshSessionHelper(
final TokenInfo newRefreshToken = RefreshToken.createNewRefreshToken(
tenantIdentifierWithStorage, main, sessionHandle,
- sessionInfo.userId, Utils.hashSHA256(refreshToken), antiCsrfToken);
+ sessionInfo.recipeUserId, Utils.hashSHA256(refreshToken), antiCsrfToken);
TokenInfo newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main,
sessionHandle,
- sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token),
+ sessionInfo.recipeUserId, sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token),
Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken,
null, accessTokenVersion, sessionInfo.useStaticKey);
@@ -630,7 +672,9 @@ private static SessionInformationHolder refreshSessionHelper(
newRefreshToken.createdTime);
return new SessionInformationHolder(
- new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()),
+ new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.recipeUserId,
+ sessionInfo.userDataInJWT,
+ tenantIdentifierWithStorage.getTenantId()),
newAccessToken, newRefreshToken, idRefreshToken, antiCsrfToken);
}
@@ -653,11 +697,11 @@ private static SessionInformationHolder refreshSessionHelper(
accessTokenVersion);
}
- throw new TokenTheftDetectedException(sessionHandle, sessionInfo.userId);
+ throw new TokenTheftDetectedException(sessionHandle, sessionInfo.recipeUserId, sessionInfo.userId);
} catch (NoSuchAlgorithmException | InvalidKeyException
- | InvalidKeySpecException | SignatureException | NoSuchPaddingException
- | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
+ | InvalidKeySpecException | SignatureException | NoSuchPaddingException
+ | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new StorageTransactionLogicException(e);
}
}
@@ -677,7 +721,8 @@ public static String[] revokeSessionUsingSessionHandles(Main main,
sessionHandles);
}
- public static String[] revokeSessionUsingSessionHandles(Main main, AppIdentifierWithStorage appIdentifierWithStorage,
+ public static String[] revokeSessionUsingSessionHandles(Main main,
+ AppIdentifierWithStorage appIdentifierWithStorage,
String[] sessionHandles)
throws StorageQueryException {
@@ -700,7 +745,8 @@ public static String[] revokeSessionUsingSessionHandles(Main main, AppIdentifier
for (String tenantId : sessionHandleMap.keySet()) {
String[] sessionHandlesForTenant = sessionHandleMap.get(tenantId).toArray(new String[0]);
- TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifierWithStorage.getConnectionUriDomain(), appIdentifierWithStorage.getAppId(), tenantId);
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifierWithStorage.getConnectionUriDomain(),
+ appIdentifierWithStorage.getAppId(), tenantId);
TenantIdentifierWithStorage tenantIdentifierWithStorage = null;
try {
tenantIdentifierWithStorage = tenantIdentifier.withStorage(
@@ -719,13 +765,14 @@ public static String[] revokeSessionUsingSessionHandles(Main main, AppIdentifier
}
private static String[] revokeSessionUsingSessionHandles(TenantIdentifierWithStorage tenantIdentifierWithStorage,
- String[] sessionHandles)
+ String[] sessionHandles)
throws StorageQueryException {
Set validHandles = new HashSet<>();
if (sessionHandles.length > 1) {
// we need to identify which sessionHandles are valid if there are more than one sessionHandles to revoke
- // if there is only one sessionHandle to revoke, we would know if it was valid by the number of revoked sessions
+ // if there is only one sessionHandle to revoke, we would know if it was valid by the number of revoked
+ // sessions
for (String sessionHandle : sessionHandles) {
if (tenantIdentifierWithStorage.getSessionStorage()
.getSession(tenantIdentifierWithStorage, sessionHandle) != null) {
@@ -761,19 +808,24 @@ private static String[] revokeSessionUsingSessionHandles(TenantIdentifierWithSto
public static String[] revokeAllSessionsForUser(Main main, String userId) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return revokeAllSessionsForUser(main,
- new AppIdentifierWithStorage(null, null, storage), userId);
+ new AppIdentifierWithStorage(null, null, storage), userId, true);
}
public static String[] revokeAllSessionsForUser(Main main, AppIdentifierWithStorage appIdentifierWithStorage,
- String userId) throws StorageQueryException {
- String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(main, appIdentifierWithStorage, userId);
+ String userId, boolean revokeSessionsForLinkedAccounts)
+ throws StorageQueryException {
+ String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(main, appIdentifierWithStorage, userId,
+ revokeSessionsForLinkedAccounts);
return revokeSessionUsingSessionHandles(main, appIdentifierWithStorage, sessionHandles);
}
public static String[] revokeAllSessionsForUser(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage,
- String userId) throws StorageQueryException {
- String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, userId);
- return revokeSessionUsingSessionHandles(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), sessionHandles);
+ String userId, boolean revokeSessionsForLinkedAccounts)
+ throws StorageQueryException {
+ String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, userId,
+ revokeSessionsForLinkedAccounts);
+ return revokeSessionUsingSessionHandles(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(),
+ sessionHandles);
}
@TestOnly
@@ -781,28 +833,45 @@ public static String[] getAllNonExpiredSessionHandlesForUser(Main main, String u
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getAllNonExpiredSessionHandlesForUser(main,
- new AppIdentifierWithStorage(null, null, storage), userId);
+ new AppIdentifierWithStorage(null, null, storage), userId, true);
}
public static String[] getAllNonExpiredSessionHandlesForUser(
- Main main, AppIdentifierWithStorage appIdentifierWithStorage, String userId)
+ Main main, AppIdentifierWithStorage appIdentifierWithStorage, String userId,
+ boolean fetchSessionsForAllLinkedAccounts)
throws StorageQueryException {
TenantConfig[] tenants = Multitenancy.getAllTenantsForApp(
appIdentifierWithStorage, main);
List sessionHandles = new ArrayList<>();
- for (TenantConfig tenant: tenants) {
- TenantIdentifierWithStorage tenantIdentifierWithStorage = null;
- try {
- tenantIdentifierWithStorage = tenant.tenantIdentifier.withStorage(
- StorageLayer.getStorage(tenant.tenantIdentifier, main));
- sessionHandles.addAll(Arrays.asList(getAllNonExpiredSessionHandlesForUser(
- tenantIdentifierWithStorage, userId)));
+ Set userIds = new HashSet<>();
+ userIds.add(userId);
+ if (fetchSessionsForAllLinkedAccounts) {
+ if (appIdentifierWithStorage.getStorage().getType().equals(STORAGE_TYPE.SQL)) {
+ AuthRecipeUserInfo primaryUser = appIdentifierWithStorage.getAuthRecipeStorage()
+ .getPrimaryUserById(appIdentifierWithStorage, userId);
+ if (primaryUser != null) {
+ for (LoginMethod lM : primaryUser.loginMethods) {
+ userIds.add(lM.getSupertokensUserId());
+ }
+ }
+ }
+ }
- } catch (TenantOrAppNotFoundException e) {
- // this might happen when a tenant was deleted after the tenant list was fetched
- // it is okay to exclude that tenant in the results here
+ for (String currUserId : userIds) {
+ for (TenantConfig tenant : tenants) {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = null;
+ try {
+ tenantIdentifierWithStorage = tenant.tenantIdentifier.withStorage(
+ StorageLayer.getStorage(tenant.tenantIdentifier, main));
+ sessionHandles.addAll(Arrays.asList(getAllNonExpiredSessionHandlesForUser(
+ tenantIdentifierWithStorage, currUserId, false)));
+
+ } catch (TenantOrAppNotFoundException e) {
+ // this might happen when a tenant was deleted after the tenant list was fetched
+ // it is okay to exclude that tenant in the results here
+ }
}
}
@@ -810,10 +879,26 @@ public static String[] getAllNonExpiredSessionHandlesForUser(
}
public static String[] getAllNonExpiredSessionHandlesForUser(
- TenantIdentifierWithStorage tenantIdentifierWithStorage, String userId)
+ TenantIdentifierWithStorage tenantIdentifierWithStorage, String userId,
+ boolean fetchSessionsForAllLinkedAccounts)
throws StorageQueryException {
- return tenantIdentifierWithStorage.getSessionStorage()
- .getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, userId);
+ Set userIds = new HashSet<>();
+ userIds.add(userId);
+ if (fetchSessionsForAllLinkedAccounts) {
+ AuthRecipeUserInfo primaryUser = tenantIdentifierWithStorage.getAuthRecipeStorage()
+ .getPrimaryUserById(tenantIdentifierWithStorage.toAppIdentifier(), userId);
+ if (primaryUser != null) {
+ for (LoginMethod lM : primaryUser.loginMethods) {
+ userIds.add(lM.getSupertokensUserId());
+ }
+ }
+ }
+ List sessionHandles = new ArrayList<>();
+ for (String currUserId : userIds) {
+ sessionHandles.addAll(List.of(tenantIdentifierWithStorage.getSessionStorage()
+ .getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, currUserId)));
+ }
+ return sessionHandles.toArray(new String[0]);
}
@TestOnly
@@ -900,7 +985,8 @@ public static void updateSession(TenantIdentifierWithStorage tenantIdentifierWit
String sessionHandle, @Nullable JsonObject sessionData,
@Nullable JsonObject jwtData, AccessToken.VERSION version)
throws StorageQueryException, UnauthorisedException, AccessTokenPayloadError {
- if (jwtData != null && Arrays.stream(AccessTokenInfo.getRequiredAndProtectedProps(version)).anyMatch(jwtData::has)) {
+ if (jwtData != null &&
+ Arrays.stream(AccessTokenInfo.getRequiredAndProtectedProps(version)).anyMatch(jwtData::has)) {
throw new AccessTokenPayloadError("The user payload contains protected field");
}
diff --git a/src/main/java/io/supertokens/session/accessToken/AccessToken.java b/src/main/java/io/supertokens/session/accessToken/AccessToken.java
index ad5fcb847..efb87b2ae 100644
--- a/src/main/java/io/supertokens/session/accessToken/AccessToken.java
+++ b/src/main/java/io/supertokens/session/accessToken/AccessToken.java
@@ -209,7 +209,7 @@ public static TokenInfo createNewAccessToken(@Nonnull Main main,
NoSuchAlgorithmException, InvalidKeySpecException, SignatureException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
- return createNewAccessToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId,
+ return createNewAccessToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId, userId,
refreshTokenHash1,
parentRefreshTokenHash1,
userData, antiCsrfToken, expiryTime, version, useStaticKey);
@@ -220,7 +220,8 @@ public static TokenInfo createNewAccessToken(@Nonnull Main main,
public static TokenInfo createNewAccessToken(TenantIdentifier tenantIdentifier, @Nonnull Main main,
@Nonnull String sessionHandle,
- @Nonnull String userId, @Nonnull String refreshTokenHash1,
+ @Nonnull String recipeUserId, @Nonnull String primaryUserId,
+ @Nonnull String refreshTokenHash1,
@Nullable String parentRefreshTokenHash1,
@Nonnull JsonObject userData, @Nullable String antiCsrfToken,
@Nullable Long expiryTime, VERSION version, boolean useStaticKey)
@@ -237,7 +238,8 @@ public static TokenInfo createNewAccessToken(TenantIdentifier tenantIdentifier,
} else {
expires = now + Config.getConfig(tenantIdentifier, main).getAccessTokenValidity();
}
- AccessTokenInfo accessToken = new AccessTokenInfo(sessionHandle, userId, refreshTokenHash1, expires,
+ AccessTokenInfo accessToken = new AccessTokenInfo(sessionHandle, recipeUserId, primaryUserId, refreshTokenHash1,
+ expires,
parentRefreshTokenHash1, userData, antiCsrfToken, now, version, tenantIdentifier);
JWTSigningKeyInfo keyToUse;
@@ -296,7 +298,8 @@ public static TokenInfo createNewAccessTokenV1(TenantIdentifier tenantIdentifier
AccessTokenInfo accessToken;
long expiryTime = now + Config.getConfig(tenantIdentifier, main).getAccessTokenValidity();
- accessToken = new AccessTokenInfo(sessionHandle, userId, refreshTokenHash1, expiryTime, parentRefreshTokenHash1,
+ accessToken = new AccessTokenInfo(sessionHandle, userId, userId, refreshTokenHash1, expiryTime,
+ parentRefreshTokenHash1,
userData, antiCsrfToken, now, VERSION.V1, tenantIdentifier);
String token = JWT.createAndSignLegacyAccessToken(accessToken.toJSON(), signingKey.privateKey,
@@ -310,6 +313,9 @@ public static VERSION getAccessTokenVersion(AccessTokenInfo accessToken) {
}
public static VERSION getAccessTokenVersionForCDI(SemVer version) {
+ if (version.greaterThanOrEqualTo(SemVer.v4_0)) {
+ return AccessToken.VERSION.V5;
+ }
if (version.greaterThanOrEqualTo(SemVer.v3_0)) {
return AccessToken.VERSION.V4;
}
@@ -354,11 +360,24 @@ public static class AccessTokenInfo {
"antiCsrfToken",
"tId"
};
+ static String[] requiredAndProtectedPropsV5 = {
+ "sub",
+ "exp",
+ "iat",
+ "sessionHandle",
+ "refreshTokenHash1",
+ "parentRefreshTokenHash1",
+ "antiCsrfToken",
+ "tId",
+ "rsub"
+ };
@Nonnull
public final String sessionHandle;
@Nonnull
- public final String userId;
+ public final String recipeUserId;
+ @Nonnull
+ public final String primaryUserId;
@Nonnull
public final String refreshTokenHash1;
@Nullable
@@ -376,12 +395,14 @@ public static class AccessTokenInfo {
@Nonnull
public TenantIdentifier tenantIdentifier;
- AccessTokenInfo(@Nonnull String sessionHandle, @Nonnull String userId, @Nonnull String refreshTokenHash1,
+ AccessTokenInfo(@Nonnull String sessionHandle, @Nonnull String recipeUserId, @Nonnull String primaryUserId,
+ @Nonnull String refreshTokenHash1,
long expiryTime, @Nullable String parentRefreshTokenHash1, @Nonnull JsonObject userData,
@Nullable String antiCsrfToken, long timeCreated, @Nonnull VERSION version,
TenantIdentifier tenantIdentifier) {
this.sessionHandle = sessionHandle;
- this.userId = userId;
+ this.recipeUserId = recipeUserId;
+ this.primaryUserId = primaryUserId;
this.refreshTokenHash1 = refreshTokenHash1;
if (version == VERSION.V2 || version == VERSION.V1) {
this.expiryTime = expiryTime;
@@ -431,9 +452,17 @@ static AccessTokenInfo fromJSON(AppIdentifier appIdentifier, JsonObject payload,
appIdentifier.getAppId(), payload.get("tId").getAsString());
}
+ String primaryUserId = payload.get("sub").getAsString();
+ String recipeUserId = payload.get("sub").getAsString();
+ if (version != VERSION.V3 && version != VERSION.V4) {
+ // this means >= v5
+ recipeUserId = payload.get("rsub").getAsString();
+ }
+
return new AccessTokenInfo(
payload.get("sessionHandle").getAsString(),
- payload.get("sub").getAsString(),
+ recipeUserId,
+ primaryUserId,
payload.get("refreshTokenHash1").getAsString(),
payload.get("exp").getAsLong() * 1000,
parentRefreshTokenHash == null || parentRefreshTokenHash.isJsonNull() ? null :
@@ -448,6 +477,7 @@ static AccessTokenInfo fromJSON(AppIdentifier appIdentifier, JsonObject payload,
return new AccessTokenInfo(
payload.get("sessionHandle").getAsString(),
payload.get("userId").getAsString(),
+ payload.get("userId").getAsString(),
payload.get("refreshTokenHash1").getAsString(),
payload.get("expiryTime").getAsLong(),
parentRefreshTokenHash == null || parentRefreshTokenHash.isJsonNull() ? null :
@@ -465,15 +495,19 @@ static AccessTokenInfo fromJSON(AppIdentifier appIdentifier, JsonObject payload,
JsonObject toJSON() throws AccessTokenPayloadError {
JsonObject res = new JsonObject();
if (this.version != VERSION.V1 && this.version != VERSION.V2) {
- res.addProperty("sub", this.userId);
+ res.addProperty("sub", this.primaryUserId);
res.addProperty("exp", this.expiryTime / 1000);
res.addProperty("iat", this.timeCreated / 1000);
if (this.version != VERSION.V3) {
res.addProperty("tId", this.tenantIdentifier.getTenantId());
}
+ if (this.version != VERSION.V3 && this.version != VERSION.V4) {
+ // this means >= v5
+ res.addProperty("rsub", this.recipeUserId);
+ }
} else {
- res.addProperty("userId", this.userId);
+ res.addProperty("userId", this.primaryUserId);
res.addProperty("expiryTime", this.expiryTime);
res.addProperty("timeCreated", this.timeCreated);
}
@@ -513,6 +547,7 @@ public static String[] getRequiredAndProtectedProps(VERSION version) {
case V1, V2 -> requiredAndProtectedPropsV2;
case V3 -> requiredAndProtectedPropsV3;
case V4 -> requiredAndProtectedPropsV4;
+ case V5 -> requiredAndProtectedPropsV5;
default -> throw new IllegalArgumentException("Unknown version: " + version);
};
}
@@ -533,7 +568,7 @@ private static void checkRequiredPropsExist(JsonObject obj, VERSION version)
}
public static VERSION getLatestVersion() {
- return VERSION.V4;
+ return VERSION.V5;
}
public static String getVersionStringFromAccessTokenVersion(VERSION version) {
@@ -541,6 +576,6 @@ public static String getVersionStringFromAccessTokenVersion(VERSION version) {
}
public enum VERSION {
- V1, V2, V3, V4
+ V1, V2, V3, V4, V5
}
}
diff --git a/src/main/java/io/supertokens/session/info/SessionInfo.java b/src/main/java/io/supertokens/session/info/SessionInfo.java
index cbb8e8719..82ed161d4 100644
--- a/src/main/java/io/supertokens/session/info/SessionInfo.java
+++ b/src/main/java/io/supertokens/session/info/SessionInfo.java
@@ -27,16 +27,22 @@ public class SessionInfo {
@Nonnull
public final String userId;
+ @Nonnull
+ public final String recipeUserId;
+
@Nonnull
public final JsonObject userDataInJWT;
@Nonnull
public final String tenantId;
- public SessionInfo(@Nonnull String handle, @Nonnull String userId, @Nonnull JsonObject userDataInJWT, @Nonnull String tenantId) {
+ public SessionInfo(@Nonnull String handle, @Nonnull String userId, @Nonnull String recipeUserId,
+ @Nonnull JsonObject userDataInJWT,
+ @Nonnull String tenantId) {
this.handle = handle;
this.userId = userId;
this.userDataInJWT = userDataInJWT;
this.tenantId = tenantId;
+ this.recipeUserId = recipeUserId;
}
}
diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java
index 5300a6c2a..33a7f8717 100644
--- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java
+++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java
@@ -17,14 +17,18 @@
package io.supertokens.thirdparty;
import io.supertokens.Main;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.exception.BadPermissionException;
+import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.Storage;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
-import io.supertokens.pluginInterface.thirdparty.UserInfo;
import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException;
import io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException;
import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage;
@@ -33,16 +37,18 @@
import org.jetbrains.annotations.TestOnly;
import javax.annotation.Nonnull;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.List;
public class ThirdParty {
public static class SignInUpResponse {
public boolean createdNewUser;
- public UserInfo user;
+ public AuthRecipeUserInfo user;
- public SignInUpResponse(boolean createdNewUser, UserInfo user) {
+ public SignInUpResponse(boolean createdNewUser, AuthRecipeUserInfo user) {
this.createdNewUser = createdNewUser;
this.user = user;
}
@@ -56,16 +62,25 @@ public static SignInUpResponse signInUp2_7(TenantIdentifierWithStorage tenantIde
String thirdPartyId, String thirdPartyUserId, String email,
boolean isEmailVerified)
throws StorageQueryException, TenantOrAppNotFoundException {
- SignInUpResponse response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId,
- email);
+ SignInUpResponse response = null;
+ try {
+ response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId,
+ email);
+ } catch (EmailChangeNotAllowedException e) {
+ throw new RuntimeException(e);
+ }
if (isEmailVerified) {
try {
+ SignInUpResponse finalResponse = response;
tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> {
try {
+ // this assert is there cause this function should only be used for older CDIs in which
+ // account linking was not available. So loginMethod length will always be 1.
+ assert (finalResponse.user.loginMethods.length == 1);
tenantIdentifierWithStorage.getEmailVerificationStorage()
.updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con,
- response.user.id, response.user.email, true);
+ finalResponse.user.getSupertokensUserId(), finalResponse.user.loginMethods[0].email, true);
tenantIdentifierWithStorage.getEmailVerificationStorage()
.commitTransaction(con);
return null;
@@ -83,7 +98,7 @@ public static SignInUpResponse signInUp2_7(TenantIdentifierWithStorage tenantIde
return response;
}
-
+
@TestOnly
public static SignInUpResponse signInUp2_7(Main main,
String thirdPartyId, String thirdPartyUserId, String email,
@@ -100,21 +115,31 @@ public static SignInUpResponse signInUp2_7(Main main,
@TestOnly
public static SignInUpResponse signInUp(Main main, String thirdPartyId, String thirdPartyUserId, String email)
- throws StorageQueryException {
+ throws StorageQueryException, EmailChangeNotAllowedException {
try {
Storage storage = StorageLayer.getStorage(main);
return signInUp(
new TenantIdentifierWithStorage(null, null, null, storage), main,
- thirdPartyId, thirdPartyUserId, email);
+ thirdPartyId, thirdPartyUserId, email, false);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
}
}
+ @TestOnly
public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
String thirdPartyId,
String thirdPartyUserId, String email)
- throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException {
+ throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException,
+ EmailChangeNotAllowedException {
+ return signInUp(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email, false);
+ }
+
+ public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main,
+ String thirdPartyId,
+ String thirdPartyUserId, String email, boolean isEmailVerified)
+ throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException,
+ EmailChangeNotAllowedException {
TenantConfig config = Multitenancy.getTenantInfo(main, tenantIdentifierWithStorage);
if (config == null) {
@@ -124,13 +149,45 @@ public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdenti
throw new BadPermissionException("Third Party login not enabled for tenant");
}
- return signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email);
+ SignInUpResponse response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId,
+ email);
+
+ if (isEmailVerified) {
+ for (LoginMethod lM : response.user.loginMethods) {
+ if (lM.thirdParty != null && lM.thirdParty.id.equals(thirdPartyId) && lM.thirdParty.userId.equals(thirdPartyUserId)) {
+ try {
+ tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> {
+ try {
+ tenantIdentifierWithStorage.getEmailVerificationStorage()
+ .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con,
+ lM.getSupertokensUserId(), lM.email, true);
+ tenantIdentifierWithStorage.getEmailVerificationStorage()
+ .commitTransaction(con);
+
+ return null;
+ } catch (TenantOrAppNotFoundException e) {
+ throw new StorageTransactionLogicException(e);
+ }
+ });
+ lM.setVerified();
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof TenantOrAppNotFoundException) {
+ throw (TenantOrAppNotFoundException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ break;
+ }
+ }
+ }
+
+ return response;
}
private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenantIdentifierWithStorage,
Main main, String thirdPartyId, String thirdPartyUserId,
String email) throws StorageQueryException,
- TenantOrAppNotFoundException {
+ TenantOrAppNotFoundException, EmailChangeNotAllowedException {
ThirdPartySQLStorage storage = tenantIdentifierWithStorage.getThirdPartyStorage();
while (true) {
// loop for sign in + sign up
@@ -141,7 +198,8 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan
long timeJoined = System.currentTimeMillis();
try {
- UserInfo createdUser = storage.signUp(tenantIdentifierWithStorage, userId, email, new UserInfo.ThirdParty(thirdPartyId, thirdPartyUserId), timeJoined);
+ AuthRecipeUserInfo createdUser = storage.signUp(tenantIdentifierWithStorage, userId, email,
+ new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId), timeJoined);
return new SignInUpResponse(true, createdUser);
} catch (DuplicateUserIdException e) {
@@ -153,69 +211,157 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan
}
// we try to get user and update their email
- SignInUpResponse response = null;
- try {
- // We should update the user email based on thirdPartyId and thirdPartyUserId across all apps,
- // so we iterate through all the app storages and do the update.
- // Note that we are only locking for each storage, and no global app wide lock, but should be okay
- // because same user parallelly logging into different tenants at the same time with different email
- // is a rare situation
- AppIdentifier appIdentifier = tenantIdentifierWithStorage.toAppIdentifier();
- Storage[] storages = StorageLayer.getStoragesForApp(main, appIdentifier);
- for (Storage st : storages) {
- storage.startTransaction(con -> {
- UserInfo user = storage.getUserInfoUsingId_Transaction(appIdentifier.withStorage(st), con,
- thirdPartyId, thirdPartyUserId);
-
- if (user == null) {
- storage.commitTransaction(con);
- return null;
+ AppIdentifier appIdentifier = tenantIdentifierWithStorage.toAppIdentifier();
+ AuthRecipeSQLStorage authRecipeStorage =
+ (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage();
+
+ { // Try without transaction, because in most cases we might not need to update the email
+ AuthRecipeUserInfo userFromDb = null;
+
+ AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo(
+ appIdentifier,
+ thirdPartyId, thirdPartyUserId);
+ for (AuthRecipeUserInfo user : usersFromDb) {
+ if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) {
+ if (userFromDb != null) {
+ throw new IllegalStateException("Should never happen");
}
+ userFromDb = user;
+ }
+ }
+ if (userFromDb == null) {
+ continue; // try to create the user again
+ }
- if (!email.equals(user.email)) {
- storage.updateUserEmail_Transaction(appIdentifier.withStorage(st), con,
- thirdPartyId, thirdPartyUserId, email);
- }
+ LoginMethod lM = null;
+ for (LoginMethod loginMethod : userFromDb.loginMethods) {
+ if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) &&
+ loginMethod.thirdParty.userId.equals(thirdPartyUserId)) {
+ lM = loginMethod;
+ break;
+ }
+ }
- storage.commitTransaction(con);
- return null;
- });
+ if (lM == null) {
+ throw new IllegalStateException("Should never come here");
}
- UserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId);
- return new SignInUpResponse(false, user);
- } catch (StorageTransactionLogicException ignored) {
- }
+ if (email.equals(lM.email)) {
+ return new SignInUpResponse(false, userFromDb);
+ } else {
+ // Email needs updating, so repeat everything in a transaction
+ try {
- if (response != null) {
- return response;
+ storage.startTransaction(con -> {
+ AuthRecipeUserInfo userFromDb1 = null;
+
+ AuthRecipeUserInfo[] usersFromDb1 = authRecipeStorage.listPrimaryUsersByThirdPartyInfo_Transaction(
+ appIdentifier,
+ con,
+ thirdPartyId, thirdPartyUserId);
+ for (AuthRecipeUserInfo user : usersFromDb1) {
+ if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) {
+ if (userFromDb1 != null) {
+ throw new IllegalStateException("Should never happen");
+ }
+ userFromDb1 = user;
+ }
+ }
+
+ if (userFromDb1 == null) {
+ storage.commitTransaction(con);
+ return null;
+ }
+
+ LoginMethod lM1 = null;
+ for (LoginMethod loginMethod : userFromDb1.loginMethods) {
+ if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) &&
+ loginMethod.thirdParty.userId.equals(thirdPartyUserId)) {
+ lM1 = loginMethod;
+ break;
+ }
+ }
+
+ if (lM1 == null) {
+ throw new IllegalStateException("Should never come here");
+ }
+
+ if (!email.equals(lM1.email)) {
+ // before updating the email, we must check for if another primary user has the same
+ // email, and if they do, then we do not allow the update.
+ if (userFromDb1.isPrimaryUser) {
+ for (String tenantId : userFromDb1.tenantIds) {
+ AuthRecipeUserInfo[] userBasedOnEmail =
+ authRecipeStorage.listPrimaryUsersByEmail_Transaction(
+ appIdentifier, con, email
+ );
+ for (AuthRecipeUserInfo userWithSameEmail : userBasedOnEmail) {
+ if (!userWithSameEmail.tenantIds.contains(tenantId)) {
+ continue;
+ }
+ if (userWithSameEmail.isPrimaryUser &&
+ !userWithSameEmail.getSupertokensUserId().equals(userFromDb1.getSupertokensUserId())) {
+ throw new StorageTransactionLogicException(
+ new EmailChangeNotAllowedException());
+ }
+ }
+ }
+ }
+ storage.updateUserEmail_Transaction(appIdentifier, con,
+ thirdPartyId, thirdPartyUserId, email);
+ }
+
+ storage.commitTransaction(con);
+ return null;
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof EmailChangeNotAllowedException) {
+ throw (EmailChangeNotAllowedException) e.actualException;
+ }
+ throw new StorageQueryException(e);
+ }
+ }
}
- // retry..
+ AuthRecipeUserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId);
+ return new SignInUpResponse(false, user);
}
}
- public static UserInfo getUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
+ @Deprecated
+ public static AuthRecipeUserInfo getUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId)
throws StorageQueryException {
- return appIdentifierWithStorage.getThirdPartyStorage()
- .getThirdPartyUserInfoUsingId(appIdentifierWithStorage, userId);
+ AuthRecipeUserInfo result = appIdentifierWithStorage.getAuthRecipeStorage()
+ .getPrimaryUserById(appIdentifierWithStorage, userId);
+ if (result == null) {
+ return null;
+ }
+ for (LoginMethod lM : result.loginMethods) {
+ if (lM.getSupertokensUserId().equals(userId) && lM.recipeId == RECIPE_ID.THIRD_PARTY) {
+ return AuthRecipeUserInfo.create(lM.getSupertokensUserId(), result.isPrimaryUser,
+ lM);
+ }
+ }
+ return null;
}
+ @Deprecated
@TestOnly
- public static UserInfo getUser(Main main, String userId) throws StorageQueryException {
+ public static AuthRecipeUserInfo getUser(Main main, String userId) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUser(new AppIdentifierWithStorage(null, null, storage), userId);
}
- public static UserInfo getUser(TenantIdentifierWithStorage tenantIdentifierWithStorage, String thirdPartyId,
- String thirdPartyUserId)
+ public static AuthRecipeUserInfo getUser(TenantIdentifierWithStorage tenantIdentifierWithStorage,
+ String thirdPartyId,
+ String thirdPartyUserId)
throws StorageQueryException {
return tenantIdentifierWithStorage.getThirdPartyStorage()
- .getThirdPartyUserInfoUsingId(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId);
+ .getPrimaryUserByThirdPartyInfo(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId);
}
@TestOnly
- public static UserInfo getUser(Main main, String thirdPartyId, String thirdPartyUserId)
+ public static AuthRecipeUserInfo getUser(Main main, String thirdPartyId, String thirdPartyUserId)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUser(
@@ -223,11 +369,21 @@ public static UserInfo getUser(Main main, String thirdPartyId, String thirdParty
thirdPartyId, thirdPartyUserId);
}
- public static UserInfo[] getUsersByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage,
- @Nonnull String email)
+ @Deprecated
+ public static AuthRecipeUserInfo[] getUsersByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage,
+ @Nonnull String email)
throws StorageQueryException {
- return tenantIdentifierWithStorage.getThirdPartyStorage()
- .getThirdPartyUsersByEmail(tenantIdentifierWithStorage, email);
+ AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getThirdPartyStorage()
+ .listPrimaryUsersByEmail(tenantIdentifierWithStorage, email);
+ List result = new ArrayList<>();
+ for (AuthRecipeUserInfo user : users) {
+ for (LoginMethod lM : user.loginMethods) {
+ if (lM.recipeId == RECIPE_ID.THIRD_PARTY && lM.email.equals(email)) {
+ result.add(user);
+ }
+ }
+ }
+ return result.toArray(new AuthRecipeUserInfo[0]);
}
public static void verifyThirdPartyProvidersArray(ThirdPartyConfig.Provider[] providers)
diff --git a/src/main/java/io/supertokens/useridmapping/UserIdMapping.java b/src/main/java/io/supertokens/useridmapping/UserIdMapping.java
index f613f73fb..687e8fea2 100644
--- a/src/main/java/io/supertokens/useridmapping/UserIdMapping.java
+++ b/src/main/java/io/supertokens/useridmapping/UserIdMapping.java
@@ -20,18 +20,23 @@
import io.supertokens.Main;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.emailverification.EmailVerificationStorage;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.jwt.JWTRecipeStorage;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.session.SessionStorage;
+import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
import io.supertokens.pluginInterface.totp.TOTPStorage;
import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage;
import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException;
import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException;
+import io.supertokens.pluginInterface.useridmapping.sqlStorage.UserIdMappingSQLStorage;
import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage;
import io.supertokens.pluginInterface.userroles.UserRolesStorage;
import io.supertokens.storageLayer.StorageLayer;
@@ -42,6 +47,8 @@
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
public class UserIdMapping {
@@ -120,17 +127,38 @@ public static io.supertokens.pluginInterface.useridmapping.UserIdMapping getUser
AppIdentifierWithStorage appIdentifierWithStorage, String userId,
UserIdType userIdType)
throws StorageQueryException {
- UserIdMappingStorage storage = appIdentifierWithStorage.getUserIdMappingStorage();
+ UserIdMappingSQLStorage storage = (UserIdMappingSQLStorage) appIdentifierWithStorage.getUserIdMappingStorage();
+
+ try {
+ return storage.startTransaction(con -> {
+ return getUserIdMapping(con, appIdentifierWithStorage, userId, userIdType);
+ });
+ } catch (StorageTransactionLogicException e) {
+ if (e.actualException instanceof StorageQueryException) {
+ throw (StorageQueryException) e.actualException;
+ } else {
+ throw new IllegalStateException(e.actualException);
+ }
+ }
+ }
+
+ public static io.supertokens.pluginInterface.useridmapping.UserIdMapping getUserIdMapping(
+ TransactionConnection con,
+ AppIdentifierWithStorage appIdentifierWithStorage, String userId,
+ UserIdType userIdType)
+ throws StorageQueryException {
+ UserIdMappingSQLStorage storage = (UserIdMappingSQLStorage) appIdentifierWithStorage.getUserIdMappingStorage();
if (userIdType == UserIdType.SUPERTOKENS) {
- return storage.getUserIdMapping(appIdentifierWithStorage, userId, true);
+ return storage.getUserIdMapping_Transaction(con, appIdentifierWithStorage, userId, true);
}
+
if (userIdType == UserIdType.EXTERNAL) {
- return storage.getUserIdMapping(appIdentifierWithStorage, userId, false);
+ return storage.getUserIdMapping_Transaction(con, appIdentifierWithStorage, userId, false);
}
- io.supertokens.pluginInterface.useridmapping.UserIdMapping[] userIdMappings = storage.getUserIdMapping(
- appIdentifierWithStorage, userId);
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping[] userIdMappings = storage.getUserIdMapping_Transaction(
+ con, appIdentifierWithStorage, userId);
if (userIdMappings.length == 0) {
return null;
@@ -261,6 +289,14 @@ public static HashMap getUserIdMappingForSuperTokensUserIds(
return tenantIdentifierWithStorage.getUserIdMappingStorage().getUserIdMappingForSuperTokensIds(userIds);
}
+ public static HashMap getUserIdMappingForSuperTokensUserIds(
+ AppIdentifierWithStorage appIdentifierWithStorage,
+ ArrayList userIds)
+ throws StorageQueryException {
+ // userIds are already filtered for a tenant, so this becomes a tenant specific operation.
+ return appIdentifierWithStorage.getUserIdMappingStorage().getUserIdMappingForSuperTokensIds(userIds);
+ }
+
@TestOnly
public static HashMap getUserIdMappingForSuperTokensUserIds(Main main,
ArrayList userIds)
@@ -321,4 +357,54 @@ public static void assertThatUserIdIsNotBeingUsedInNonAuthRecipes(
}
}
}
+
+ public static void populateExternalUserIdForUsers(AppIdentifierWithStorage appIdentifierWithStorage, AuthRecipeUserInfo[] users)
+ throws StorageQueryException {
+ Set userIds = new HashSet<>();
+
+ for (AuthRecipeUserInfo user : users) {
+ userIds.add(user.getSupertokensUserId());
+
+ for (LoginMethod lm : user.loginMethods) {
+ userIds.add(lm.getSupertokensUserId());
+ }
+ }
+ ArrayList userIdsList = new ArrayList<>(userIds);
+ userIdsList.addAll(userIds);
+ HashMap userIdMappings = getUserIdMappingForSuperTokensUserIds(appIdentifierWithStorage,
+ userIdsList);
+
+ for (AuthRecipeUserInfo user : users) {
+ user.setExternalUserId(userIdMappings.get(user.getSupertokensUserId()));
+
+ for (LoginMethod lm : user.loginMethods) {
+ lm.setExternalUserId(userIdMappings.get(lm.getSupertokensUserId()));
+ }
+ }
+ }
+
+ public static void populateExternalUserIdForUsers(TenantIdentifierWithStorage tenantIdentifierWithStorage, AuthRecipeUserInfo[] users)
+ throws StorageQueryException {
+ Set userIds = new HashSet<>();
+
+ for (AuthRecipeUserInfo user : users) {
+ userIds.add(user.getSupertokensUserId());
+
+ for (LoginMethod lm : user.loginMethods) {
+ userIds.add(lm.getSupertokensUserId());
+ }
+ }
+ ArrayList userIdsList = new ArrayList<>(userIds);
+ userIdsList.addAll(userIds);
+ HashMap userIdMappings = getUserIdMappingForSuperTokensUserIds(tenantIdentifierWithStorage,
+ userIdsList);
+
+ for (AuthRecipeUserInfo user : users) {
+ user.setExternalUserId(userIdMappings.get(user.getSupertokensUserId()));
+
+ for (LoginMethod lm : user.loginMethods) {
+ lm.setExternalUserId(userIdMappings.get(lm.getSupertokensUserId()));
+ }
+ }
+ }
}
diff --git a/src/main/java/io/supertokens/utils/SemVer.java b/src/main/java/io/supertokens/utils/SemVer.java
index e5ab842d3..64b63ace6 100644
--- a/src/main/java/io/supertokens/utils/SemVer.java
+++ b/src/main/java/io/supertokens/utils/SemVer.java
@@ -33,6 +33,7 @@ public class SemVer implements Comparable {
public static final SemVer v2_20 = new SemVer("2.20");
public static final SemVer v2_21 = new SemVer("2.21");
public static final SemVer v3_0 = new SemVer("3.0");
+ public static final SemVer v4_0 = new SemVer("4.0");
final private String version;
@@ -41,11 +42,11 @@ public final String get() {
}
public SemVer(String version) {
- if(version == null) {
+ if (version == null) {
throw new IllegalArgumentException("Version can not be null");
}
- if(!version.matches("[0-9]+(\\.[0-9]+)*")) {
+ if (!version.matches("[0-9]+(\\.[0-9]+)*")) {
throw new IllegalArgumentException("Invalid version format");
}
@@ -68,7 +69,8 @@ public boolean lesserThan(SemVer max) {
return this.compareTo(max) < 0;
}
- @Override public int compareTo(SemVer that) {
+ @Override
+ public int compareTo(SemVer that) {
if (that == null) {
return 1;
}
@@ -82,11 +84,11 @@ public boolean lesserThan(SemVer max) {
int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0;
int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0;
- if(thisPart < thatPart) {
+ if (thisPart < thatPart) {
return -1;
}
- if(thisPart > thatPart) {
+ if (thisPart > thatPart) {
return 1;
}
}
@@ -94,16 +96,17 @@ public boolean lesserThan(SemVer max) {
return 0;
}
- @Override public boolean equals(Object that) {
- if(this == that) {
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
return true;
}
- if(that == null) {
+ if (that == null) {
return false;
}
- if(!(that instanceof SemVer)) {
+ if (!(that instanceof SemVer)) {
return false;
}
diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java
index 8184bde18..5e4a4ff8c 100644
--- a/src/main/java/io/supertokens/webserver/Webserver.java
+++ b/src/main/java/io/supertokens/webserver/Webserver.java
@@ -26,6 +26,7 @@
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.webserver.api.accountlinking.*;
import io.supertokens.webserver.api.core.*;
import io.supertokens.webserver.api.dashboard.*;
import io.supertokens.webserver.api.emailpassword.UserAPI;
@@ -37,10 +38,6 @@
import io.supertokens.webserver.api.jwt.JWKSAPI;
import io.supertokens.webserver.api.jwt.JWTSigningAPI;
import io.supertokens.webserver.api.multitenancy.*;
-import io.supertokens.webserver.api.multitenancy.CreateOrUpdateAppAPI;
-import io.supertokens.webserver.api.multitenancy.CreateOrUpdateConnectionUriDomainAPI;
-import io.supertokens.webserver.api.multitenancy.CreateOrUpdateTenantOrGetTenantAPI;
-import io.supertokens.webserver.api.multitenancy.RemoveTenantAPI;
import io.supertokens.webserver.api.multitenancy.thirdparty.CreateOrUpdateThirdPartyConfigAPI;
import io.supertokens.webserver.api.multitenancy.thirdparty.RemoveThirdPartyConfigAPI;
import io.supertokens.webserver.api.passwordless.*;
@@ -250,6 +247,16 @@ private void setupRoutes() {
addAPI(new AssociateUserToTenantAPI(main));
addAPI(new DisassociateUserFromTenant(main));
+ addAPI(new GetUserByIdAPI(main));
+ addAPI(new ListUsersByAccountInfoAPI(main));
+
+ addAPI(new CanCreatePrimaryUserAPI(main));
+ addAPI(new CreatePrimaryUserAPI(main));
+ addAPI(new CanLinkAccountsAPI(main));
+ addAPI(new LinkAccountsAPI(main));
+ addAPI(new UnlinkAccountAPI(main));
+ addAPI(new ConsumeResetPasswordAPI(main));
+
StandardContext context = tomcatReference.getContext();
Tomcat tomcat = tomcatReference.getTomcat();
diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java
index ec9c876ce..fb20f98fe 100644
--- a/src/main/java/io/supertokens/webserver/WebserverAPI.java
+++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java
@@ -75,10 +75,11 @@ public abstract class WebserverAPI extends HttpServlet {
supportedVersions.add(SemVer.v2_20);
supportedVersions.add(SemVer.v2_21);
supportedVersions.add(SemVer.v3_0);
+ supportedVersions.add(SemVer.v4_0);
}
public static SemVer getLatestCDIVersion() {
- return SemVer.v3_0;
+ return SemVer.v4_0;
}
public SemVer getLatestCDIVersionForRequest(HttpServletRequest req)
diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CanCreatePrimaryUserAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CanCreatePrimaryUserAPI.java
new file mode 100644
index 000000000..9457ddd0d
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CanCreatePrimaryUserAPI.java
@@ -0,0 +1,113 @@
+/*
+ * 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 io.supertokens.webserver.api.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class CanCreatePrimaryUserAPI extends WebserverAPI {
+
+ public CanCreatePrimaryUserAPI(Main main) {
+ super(main, RECIPE_ID.ACCOUNT_LINKING.toString());
+ }
+
+ @Override
+ public String getPath() {
+ return "/recipe/accountlinking/user/primary/check";
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is app specific
+ String inputRecipeUserId = InputParser.getQueryParamOrThrowError(req, "recipeUserId", false);
+
+ AppIdentifierWithStorage appIdentifierWithStorage = null;
+ try {
+ String userId = inputRecipeUserId;
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputRecipeUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ userId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.canCreatePrimaryUser(appIdentifierWithStorage,
+ userId);
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "OK");
+ response.addProperty("wasAlreadyAPrimaryUser", result.wasAlreadyAPrimaryUser);
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException | TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ } catch (UnknownUserIdException e) {
+ throw new ServletException(new BadRequestException("Unknown user ID provided"));
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR");
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping(
+ appIdentifierWithStorage, e.primaryUserId,
+ UserIdType.SUPERTOKENS);
+ if (result != null) {
+ response.addProperty("primaryUserId", result.externalUserId);
+ } else {
+ response.addProperty("primaryUserId", e.primaryUserId);
+ }
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ } catch (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR");
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping(
+ appIdentifierWithStorage, e.primaryUserId,
+ UserIdType.SUPERTOKENS);
+ if (result != null) {
+ response.addProperty("primaryUserId", result.externalUserId);
+ } else {
+ response.addProperty("primaryUserId", e.primaryUserId);
+ }
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java
new file mode 100644
index 000000000..97c10f2c6
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java
@@ -0,0 +1,138 @@
+/*
+ * 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 io.supertokens.webserver.api.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class CanLinkAccountsAPI extends WebserverAPI {
+
+ public CanLinkAccountsAPI(Main main) {
+ super(main, RECIPE_ID.ACCOUNT_LINKING.toString());
+ }
+
+ @Override
+ public String getPath() {
+ return "/recipe/accountlinking/user/link/check";
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is app specific
+ String inputRecipeUserId = InputParser.getQueryParamOrThrowError(req, "recipeUserId", false);
+ String inputPrimaryUserId = InputParser.getQueryParamOrThrowError(req, "primaryUserId", false);
+
+ AppIdentifierWithStorage primaryUserIdAppIdentifierWithStorage = null;
+ AppIdentifierWithStorage recipeUserIdAppIdentifierWithStorage = null;
+ try {
+ String recipeUserId = inputRecipeUserId;
+ {
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputRecipeUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ recipeUserId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ recipeUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+ }
+ String primaryUserId = inputPrimaryUserId;
+ {
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputPrimaryUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ primaryUserId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ primaryUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+ }
+
+ // we do a check based on user pool ID and not instance reference checks cause the user
+ // could be in the same db, but their storage layers may just have different
+ if (!primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId().equals(
+ recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId())) {
+ throw new ServletException(
+ new BadRequestException(
+ "Cannot link users that are parts of different databases. Different pool IDs: " +
+ primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId() + " AND " +
+ recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId()));
+ }
+
+ AuthRecipe.CanLinkAccountsResult result = AuthRecipe.canLinkAccounts(primaryUserIdAppIdentifierWithStorage,
+ recipeUserId, primaryUserId);
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "OK");
+ response.addProperty("accountsAlreadyLinked", result.alreadyLinked);
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException | TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ } catch (UnknownUserIdException e) {
+ throw new ServletException(new BadRequestException("Unknown user ID provided"));
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR");
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping(
+ primaryUserIdAppIdentifierWithStorage, e.primaryUserId,
+ UserIdType.SUPERTOKENS);
+ if (result != null) {
+ response.addProperty("primaryUserId", result.externalUserId);
+ } else {
+ response.addProperty("primaryUserId", e.primaryUserId);
+ }
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ } catch (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR");
+ UserIdMapping.populateExternalUserIdForUsers(recipeUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{e.recipeUser});
+ response.addProperty("primaryUserId", e.recipeUser.getSupertokensOrExternalUserId());
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ } catch (InputUserIdIsNotAPrimaryUserException e) {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "INPUT_USER_IS_NOT_A_PRIMARY_USER");
+ super.sendJsonResponse(200, response, resp);
+ }
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CreatePrimaryUserAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CreatePrimaryUserAPI.java
new file mode 100644
index 000000000..8573650b7
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CreatePrimaryUserAPI.java
@@ -0,0 +1,121 @@
+/*
+ * 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 io.supertokens.webserver.api.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException;
+import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class CreatePrimaryUserAPI extends WebserverAPI {
+
+ public CreatePrimaryUserAPI(Main main) {
+ super(main, RECIPE_ID.ACCOUNT_LINKING.toString());
+ }
+
+ @Override
+ public String getPath() {
+ return "/recipe/accountlinking/user/primary";
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is app specific
+ JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
+ String inputRecipeUserId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+
+ AppIdentifierWithStorage appIdentifierWithStorage = null;
+ try {
+ String userId = inputRecipeUserId;
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputRecipeUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ userId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(main, appIdentifierWithStorage,
+ userId);
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "OK");
+ response.addProperty("wasAlreadyAPrimaryUser", result.wasAlreadyAPrimaryUser);
+ if (mappingAndStorage.userIdMapping != null) {
+ result.user.setExternalUserId(mappingAndStorage.userIdMapping.externalUserId);
+ } else {
+ result.user.setExternalUserId(null);
+ }
+ response.add("user", result.user.toJson());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException e) {
+ throw new ServletException(e);
+ } catch (UnknownUserIdException e) {
+ throw new ServletException(new BadRequestException("Unknown user ID provided"));
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR");
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping(
+ appIdentifierWithStorage, e.primaryUserId,
+ UserIdType.SUPERTOKENS);
+ if (result != null) {
+ response.addProperty("primaryUserId", result.externalUserId);
+ } else {
+ response.addProperty("primaryUserId", e.primaryUserId);
+ }
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ } catch (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR");
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping(
+ appIdentifierWithStorage, e.primaryUserId,
+ UserIdType.SUPERTOKENS);
+ if (result != null) {
+ response.addProperty("primaryUserId", result.externalUserId);
+ } else {
+ response.addProperty("primaryUserId", e.primaryUserId);
+ }
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java
new file mode 100644
index 000000000..76a9d4dfe
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java
@@ -0,0 +1,149 @@
+/*
+ * 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 io.supertokens.webserver.api.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ActiveUsers;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
+import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class LinkAccountsAPI extends WebserverAPI {
+
+ public LinkAccountsAPI(Main main) {
+ super(main, RECIPE_ID.ACCOUNT_LINKING.toString());
+ }
+
+ @Override
+ public String getPath() {
+ return "/recipe/accountlinking/user/link";
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is app specific
+ JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
+ String inputRecipeUserId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+ String inputPrimaryUserId = InputParser.parseStringOrThrowError(input, "primaryUserId", false);
+
+ AppIdentifierWithStorage primaryUserIdAppIdentifierWithStorage = null;
+ AppIdentifierWithStorage recipeUserIdAppIdentifierWithStorage = null;
+ try {
+ String recipeUserId = inputRecipeUserId;
+ {
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputRecipeUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ recipeUserId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ recipeUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+ }
+ String primaryUserId = inputPrimaryUserId;
+ {
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputPrimaryUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ primaryUserId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ primaryUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+ }
+
+ // we do a check based on user pool ID and not instance reference checks cause the user
+ // could be in the same db, but their storage layers may just have different
+ if (!primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId().equals(
+ recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId())) {
+ throw new ServletException(
+ new BadRequestException(
+ "Cannot link users that are parts of different databases. Different pool IDs: " +
+ primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId() + " AND " +
+ recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId()));
+ }
+
+ AuthRecipe.LinkAccountsResult linkAccountsResult = AuthRecipe.linkAccounts(main,
+ primaryUserIdAppIdentifierWithStorage,
+ recipeUserId, primaryUserId);
+
+ UserIdMapping.populateExternalUserIdForUsers(primaryUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{linkAccountsResult.user});
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "OK");
+ response.addProperty("accountsAlreadyLinked", linkAccountsResult.wasAlreadyLinked);
+ response.add("user", linkAccountsResult.user.toJson());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException e) {
+ throw new ServletException(e);
+ } catch (UnknownUserIdException e) {
+ throw new ServletException(new BadRequestException("Unknown user ID provided"));
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR");
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping(
+ primaryUserIdAppIdentifierWithStorage, e.primaryUserId,
+ UserIdType.SUPERTOKENS);
+ if (result != null) {
+ response.addProperty("primaryUserId", result.externalUserId);
+ } else {
+ response.addProperty("primaryUserId", e.primaryUserId);
+ }
+ response.addProperty("description", e.getMessage());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ } catch (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException e) {
+ try {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR");
+ UserIdMapping.populateExternalUserIdForUsers(recipeUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{e.recipeUser});
+ response.addProperty("primaryUserId", e.recipeUser.getSupertokensOrExternalUserId());
+ response.addProperty("description", e.getMessage());
+ response.add("user", e.recipeUser.toJson());
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException ex) {
+ throw new ServletException(ex);
+ }
+ } catch (InputUserIdIsNotAPrimaryUserException e) {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "INPUT_USER_IS_NOT_A_PRIMARY_USER");
+ super.sendJsonResponse(200, response, resp);
+ }
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/UnlinkAccountAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/UnlinkAccountAPI.java
new file mode 100644
index 000000000..3abed0328
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/accountlinking/UnlinkAccountAPI.java
@@ -0,0 +1,85 @@
+/*
+ * 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 io.supertokens.webserver.api.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class UnlinkAccountAPI extends WebserverAPI {
+
+ public UnlinkAccountAPI(Main main) {
+ super(main, RECIPE_ID.ACCOUNT_LINKING.toString());
+ }
+
+ @Override
+ public String getPath() {
+ return "/recipe/accountlinking/user/unlink";
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is app specific
+ JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
+ String inputRecipeUserId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+
+ AppIdentifierWithStorage appIdentifierWithStorage = null;
+ try {
+ String userId = inputRecipeUserId;
+ AppIdentifierWithStorageAndUserIdMapping mappingAndStorage =
+ getAppIdentifierWithStorageAndUserIdMappingFromRequest(
+ req, inputRecipeUserId, UserIdType.ANY);
+ if (mappingAndStorage.userIdMapping != null) {
+ userId = mappingAndStorage.userIdMapping.superTokensUserId;
+ }
+ appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage;
+
+ boolean wasDeleted = AuthRecipe.unlinkAccounts(main, appIdentifierWithStorage,
+ userId);
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "OK");
+ response.addProperty("wasRecipeUserDeleted", wasDeleted);
+ response.addProperty("wasLinked", true);
+ super.sendJsonResponse(200, response, resp);
+ } catch (StorageQueryException | TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ } catch (UnknownUserIdException e) {
+ throw new ServletException(new BadRequestException("Unknown user ID provided"));
+ } catch (InputUserIdIsNotAPrimaryUserException e) {
+ JsonObject response = new JsonObject();
+ response.addProperty("status", "OK");
+ response.addProperty("wasRecipeUserDeleted", false);
+ response.addProperty("wasLinked", false);
+ super.sendJsonResponse(200, response, resp);
+ }
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java b/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java
index 166b361da..ce51410f3 100644
--- a/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java
@@ -51,11 +51,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
// this API is app specific
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
String userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ Boolean removeAllLinkedAccounts = InputParser.parseBooleanOrThrowError(input, "removeAllLinkedAccounts", true);
+
+ if (removeAllLinkedAccounts == null) {
+ removeAllLinkedAccounts = true;
+ }
+
try {
AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping =
this.getAppIdentifierWithStorageAndUserIdMappingFromRequest(req, userId, UserIdType.ANY);
AuthRecipe.deleteUser(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, userId,
+ removeAllLinkedAccounts,
appIdentifierWithStorageAndUserIdMapping.userIdMapping);
} catch (StorageQueryException | TenantOrAppNotFoundException | StorageTransactionLogicException e) {
throw new ServletException(e);
diff --git a/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java b/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java
new file mode 100644
index 000000000..e2562c8fa
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java
@@ -0,0 +1,95 @@
+/*
+ * 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 io.supertokens.webserver.api.core;
+
+import com.google.gson.JsonObject;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class GetUserByIdAPI extends WebserverAPI {
+
+ public GetUserByIdAPI(Main main) {
+ super(main, "");
+ }
+
+ @Override
+ public String getPath() {
+ return "/user/id";
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is app specific
+ String userId = InputParser.getQueryParamOrThrowError(req, "userId", false);
+
+ try {
+ AuthRecipeUserInfo user = null;
+
+ try {
+ AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping =
+ this.getAppIdentifierWithStorageAndUserIdMappingFromRequest(req, userId, UserIdType.ANY);
+ // if a userIdMapping exists, pass the superTokensUserId to the getUserUsingId function
+ if (appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) {
+ userId = appIdentifierWithStorageAndUserIdMapping.userIdMapping.superTokensUserId;
+ }
+
+ user = AuthRecipe.getUserById(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage,
+ userId);
+
+ // if a userIdMapping exists, set the userId in the response to the externalUserId
+ if (user != null) {
+ UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user});
+ }
+
+ } catch (UnknownUserIdException e) {
+ // ignore the error so that the use can remain a null
+ }
+
+ if (user == null) {
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "UNKNOWN_USER_ID_ERROR");
+ super.sendJsonResponse(200, result, resp);
+
+ } else {
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "OK");
+ JsonObject userJson = user.toJson();
+
+ result.add("user", userJson);
+ super.sendJsonResponse(200, result, resp);
+ }
+
+ } catch (StorageQueryException | TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ }
+
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java b/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java
new file mode 100644
index 000000000..2cc2a7eb5
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java
@@ -0,0 +1,96 @@
+/*
+ * 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 io.supertokens.webserver.api.core;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import io.supertokens.Main;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.utils.Utils;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class ListUsersByAccountInfoAPI extends WebserverAPI {
+
+ public ListUsersByAccountInfoAPI(Main main) {
+ super(main, "");
+ }
+
+ @Override
+ public String getPath() {
+ return "/users/by-accountinfo";
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is tenant specific.
+ String email = InputParser.getQueryParamOrThrowError(req, "email", true);
+ String phoneNumber = InputParser.getQueryParamOrThrowError(req, "phoneNumber", true);
+ String thirdPartyId = InputParser.getQueryParamOrThrowError(req, "thirdPartyId", true);
+ String thirdPartyUserId = InputParser.getQueryParamOrThrowError(req, "thirdPartyUserId", true);
+
+ String doUnionOfAccountInfoStr = InputParser.getQueryParamOrThrowError(req, "doUnionOfAccountInfo", false);
+ if (!(doUnionOfAccountInfoStr.equals("false") || doUnionOfAccountInfoStr.equals("true"))) {
+ throw new ServletException(new BadRequestException(
+ "'doUnionOfAccountInfo' should be either 'true' or 'false'"));
+ }
+ boolean doUnionOfAccountInfo = doUnionOfAccountInfoStr.equals("true");
+
+ if (email != null) {
+ email = Utils.normaliseEmail(email);
+ }
+ if (thirdPartyId != null || thirdPartyUserId != null) {
+ if (thirdPartyId == null || thirdPartyUserId == null) {
+ throw new ServletException(new BadRequestException(
+ "If 'thirdPartyId' is provided, 'thirdPartyUserId' must also be provided, and vice versa"));
+ }
+ }
+
+ try {
+ AppIdentifierWithStorage appIdentifierWithStorage = this.getAppIdentifierWithStorage(req);
+ AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(
+ this.getTenantIdentifierWithStorageFromRequest(
+ req), doUnionOfAccountInfo, email, phoneNumber, thirdPartyId, thirdPartyUserId);
+ UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorage, users);
+
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "OK");
+ JsonArray usersJson = new JsonArray();
+ for (AuthRecipeUserInfo userInfo : users) {
+ usersJson.add(userInfo.toJson());
+ }
+
+ result.add("users", usersJson);
+ super.sendJsonResponse(200, result, resp);
+
+ } catch (StorageQueryException | TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ }
+
+ }
+}
diff --git a/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java b/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java
index 9184eec20..4e7e2120c 100644
--- a/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java
@@ -16,13 +16,16 @@
package io.supertokens.webserver.api.core;
-import com.google.gson.*;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.authRecipe.UserPaginationContainer;
import io.supertokens.authRecipe.UserPaginationToken;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
@@ -165,25 +168,23 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
limit, timeJoinedOrder, paginationToken,
recipeIdsEnumBuilder.build().toArray(RECIPE_ID[]::new), searchTags);
- ArrayList userIds = new ArrayList<>();
- for (int i = 0; i < users.users.length; i++) {
- userIds.add(users.users[i].user.id);
- }
- HashMap userIdMapping = UserIdMapping.getUserIdMappingForSuperTokensUserIds(
- tenantIdentifierWithStorage, userIds);
- if (!userIdMapping.isEmpty()) {
- for (int i = 0; i < users.users.length; i++) {
- String externalId = userIdMapping.get(userIds.get(i));
- if (externalId != null) {
- users.users[i].user.id = externalId;
- }
- }
- }
+ UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, users.users);
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonArray usersJson = new JsonParser().parse(new Gson().toJson(users.users)).getAsJsonArray();
+ JsonArray usersJson = new JsonArray();
+ for (AuthRecipeUserInfo user : users.users) {
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ JsonObject jsonObj = new JsonObject();
+ jsonObj.addProperty("recipeId", user.loginMethods[0].recipeId.toString());
+ JsonObject userJson = user.toJsonWithoutAccountLinking();
+ jsonObj.add("user", userJson);
+ usersJson.add(jsonObj);
+ } else {
+ usersJson.add(user.toJson());
+ }
+ }
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
for (JsonElement user : usersJson) {
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/ConsumeResetPasswordAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/ConsumeResetPasswordAPI.java
new file mode 100644
index 000000000..362ce9020
--- /dev/null
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ConsumeResetPasswordAPI.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2020, 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 io.supertokens.webserver.api.emailpassword;
+
+import com.google.gson.JsonObject;
+import io.supertokens.Main;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.ResetPasswordInvalidTokenException;
+import io.supertokens.output.Logging;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.utils.Utils;
+import io.supertokens.webserver.InputParser;
+import io.supertokens.webserver.WebserverAPI;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+public class ConsumeResetPasswordAPI extends WebserverAPI {
+ private static final long serialVersionUID = -7529428297450682549L;
+
+ public ConsumeResetPasswordAPI(Main main) {
+ super(main, RECIPE_ID.EMAIL_PASSWORD.toString());
+ }
+
+ @Override
+ public String getPath() {
+ return "/recipe/user/password/reset/token/consume";
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
+ // API is tenant specific
+ JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
+ String token = InputParser.parseStringOrThrowError(input, "token", false);
+ assert token != null;
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = null;
+ try {
+ tenantIdentifierWithStorage = getTenantIdentifierWithStorageFromRequest(req);
+ } catch (TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ }
+
+ try {
+ EmailPassword.ConsumeResetPasswordTokenResult result = EmailPassword.consumeResetPasswordToken(
+ tenantIdentifierWithStorage, token);
+
+ io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping.getUserIdMapping(
+ tenantIdentifierWithStorage.toAppIdentifierWithStorage(), result.userId, UserIdType.SUPERTOKENS);
+
+ // if userIdMapping exists, pass the externalUserId to the response
+ if (userIdMapping != null) {
+ result.userId = userIdMapping.externalUserId;
+ }
+
+ JsonObject resultJson = new JsonObject();
+ resultJson.addProperty("status", "OK");
+ resultJson.addProperty("userId", result.userId);
+ resultJson.addProperty("email", result.email);
+
+ super.sendJsonResponse(200, resultJson, resp);
+
+ } catch (ResetPasswordInvalidTokenException e) {
+ Logging.debug(main, tenantIdentifierWithStorage, Utils.exceptionStacktraceToString(e));
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "RESET_PASSWORD_INVALID_TOKEN_ERROR");
+ super.sendJsonResponse(200, result, resp);
+
+ } catch (StorageQueryException | NoSuchAlgorithmException | StorageTransactionLogicException |
+ TenantOrAppNotFoundException e) {
+ throw new ServletException(e);
+ }
+
+ }
+
+}
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java
index b5d97500e..24f55a679 100644
--- a/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java
@@ -18,6 +18,7 @@
import com.google.gson.JsonObject;
import io.supertokens.Main;
+import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping;
import io.supertokens.emailpassword.EmailPassword;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.output.Logging;
@@ -25,10 +26,9 @@
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
-import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
-import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping;
import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.utils.SemVer;
import io.supertokens.utils.Utils;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
@@ -58,7 +58,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
// API is tenant specific
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
String userId = InputParser.parseStringOrThrowError(input, "userId", false);
- assert userId != null;
// logic according to https://github.com/supertokens/supertokens-core/issues/106
TenantIdentifier tenantIdentifier = null;
@@ -76,7 +75,15 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
userId = tenantIdentifierStorageAndMapping.userIdMapping.superTokensUserId;
}
- String token = EmailPassword.generatePasswordResetToken(tenantIdentifierStorageAndMapping.tenantIdentifierWithStorage, super.main, userId);
+ String token;
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ String email = InputParser.parseStringOrThrowError(input, "email", false);
+ token = EmailPassword.generatePasswordResetToken(
+ tenantIdentifierStorageAndMapping.tenantIdentifierWithStorage, super.main, userId, email);
+ } else {
+ token = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(
+ tenantIdentifierStorageAndMapping.tenantIdentifierWithStorage, super.main, userId);
+ }
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
@@ -89,7 +96,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
result.addProperty("status", "UNKNOWN_USER_ID_ERROR");
super.sendJsonResponse(200, result, resp);
- } catch (StorageQueryException | BadPermissionException | NoSuchAlgorithmException | InvalidKeySpecException | TenantOrAppNotFoundException e) {
+ } catch (StorageQueryException | BadPermissionException | NoSuchAlgorithmException | InvalidKeySpecException |
+ TenantOrAppNotFoundException | BadRequestException e) {
throw new ServletException(e);
}
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java
index cc71246f2..f1d9a7a9a 100644
--- a/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java
@@ -16,18 +16,19 @@
package io.supertokens.webserver.api.emailpassword;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import io.supertokens.Main;
import io.supertokens.config.CoreConfig;
import io.supertokens.emailpassword.EmailPassword;
import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.useridmapping.UserIdMapping;
import io.supertokens.utils.SemVer;
import io.supertokens.utils.Utils;
import io.supertokens.webserver.InputParser;
@@ -93,12 +94,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
}
try {
+ TenantIdentifierWithStorage tenant = this.getTenantIdentifierWithStorageFromRequest(req);
EmailPassword.ImportUserResponse importUserResponse = EmailPassword.importUserWithPasswordHash(
- this.getTenantIdentifierWithStorageFromRequest(req), main, email,
+ tenant, main, email,
passwordHash, passwordHashingAlgorithm);
+ UserIdMapping.populateExternalUserIdForUsers(getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{importUserResponse.user});
JsonObject response = new JsonObject();
response.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(importUserResponse.user)).getAsJsonObject();
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? importUserResponse.user.toJson() :
+ importUserResponse.user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
@@ -107,7 +112,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
response.add("user", userJson);
response.addProperty("didUserAlreadyExist", importUserResponse.didUserAlreadyExist);
super.sendJsonResponse(200, response, resp);
- } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | BadPermissionException e) {
+ } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException |
+ BadPermissionException e) {
throw new ServletException(e);
} catch (UnsupportedPasswordHashingFormatException e) {
throw new ServletException(new WebserverAPI.BadRequestException(e.getMessage()));
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java
index a46b58905..05d354676 100644
--- a/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java
@@ -24,7 +24,6 @@
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
-import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.useridmapping.UserIdMapping;
@@ -40,6 +39,7 @@
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
+@Deprecated
public class ResetPasswordAPI extends WebserverAPI {
private static final long serialVersionUID = -7529428297450682549L;
@@ -94,8 +94,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- if (!(super.getVersionFromRequest(req).equals(SemVer.v2_7) || super.getVersionFromRequest(req).equals(SemVer.v2_8)
- || super.getVersionFromRequest(req).equals(SemVer.v2_9) || super.getVersionFromRequest(req).equals(SemVer.v2_10)
+ if (!(super.getVersionFromRequest(req).equals(SemVer.v2_7) ||
+ super.getVersionFromRequest(req).equals(SemVer.v2_8)
+ || super.getVersionFromRequest(req).equals(SemVer.v2_9) ||
+ super.getVersionFromRequest(req).equals(SemVer.v2_10)
|| super.getVersionFromRequest(req).equals(SemVer.v2_11))) {
// >= 2.12
result.addProperty("userId", userId);
@@ -109,7 +111,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
result.addProperty("status", "RESET_PASSWORD_INVALID_TOKEN_ERROR");
super.sendJsonResponse(200, result, resp);
- } catch (StorageQueryException | NoSuchAlgorithmException | StorageTransactionLogicException | TenantOrAppNotFoundException e) {
+ } catch (StorageQueryException | NoSuchAlgorithmException | StorageTransactionLogicException |
+ TenantOrAppNotFoundException e) {
throw new ServletException(e);
}
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java
index 8f2568d0b..cf57898cd 100644
--- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java
@@ -16,10 +16,7 @@
package io.supertokens.webserver.api.emailpassword;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
import io.supertokens.ActiveUsers;
import io.supertokens.Main;
import io.supertokens.emailpassword.EmailPassword;
@@ -27,7 +24,8 @@
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.RECIPE_ID;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
@@ -77,25 +75,30 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
}
try {
- UserInfo user = EmailPassword.signIn(tenantIdentifierWithStorage, super.main, normalisedEmail, password);
-
- ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, user.id); // use the internal user id
-
- // if a userIdMapping exists, pass the externalUserId to the response
- UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
- tenantIdentifierWithStorage.toAppIdentifierWithStorage(), user.id, UserIdType.SUPERTOKENS);
+ AuthRecipeUserInfo user = EmailPassword.signIn(tenantIdentifierWithStorage, super.main, normalisedEmail,
+ password);
+ io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, new AuthRecipeUserInfo[]{user});
- if (userIdMapping != null) {
- user.id = userIdMapping.externalUserId;
- }
+ ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main,
+ user.getSupertokensUserId()); // use the internal user id
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject();
+ JsonObject userJson = getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() :
+ user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
}
result.add("user", userJson);
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ for (LoginMethod loginMethod : user.loginMethods) {
+ if (loginMethod.recipeId.equals(RECIPE_ID.EMAIL_PASSWORD) && normalisedEmail.equals(loginMethod.email)) {
+ result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId());
+ break;
+ }
+ }
+ }
+
super.sendJsonResponse(200, result, resp);
} catch (WrongCredentialsException e) {
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java
index 8c184b021..bb915fadb 100644
--- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java
@@ -16,20 +16,18 @@
package io.supertokens.webserver.api.emailpassword;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
import io.supertokens.ActiveUsers;
import io.supertokens.Main;
import io.supertokens.emailpassword.EmailPassword;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.RECIPE_ID;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.utils.SemVer;
import io.supertokens.utils.Utils;
@@ -79,21 +77,27 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
}
try {
- UserInfo user = EmailPassword.signUp(this.getTenantIdentifierWithStorageFromRequest(req), super.main, normalisedEmail, password);
+ TenantIdentifierWithStorage tenant = this.getTenantIdentifierWithStorageFromRequest(req);
+ AuthRecipeUserInfo user = EmailPassword.signUp(tenant, super.main, normalisedEmail, password);
- ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.id);
+ ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.getSupertokensUserId());
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject();
+ user.setExternalUserId(null);
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() :
+ user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
}
result.add("user", userJson);
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ result.addProperty("recipeUserId", user.getSupertokensOrExternalUserId());
+ }
super.sendJsonResponse(200, result, resp);
-
} catch (DuplicateEmailException e) {
Logging.debug(main, tenantIdentifier, Utils.exceptionStacktraceToString(e));
JsonObject result = new JsonObject();
diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java
index d3503ed2c..7f992f6a5 100644
--- a/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java
@@ -16,14 +16,14 @@
package io.supertokens.webserver.api.emailpassword;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
+import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
import io.supertokens.Main;
import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.RECIPE_ID;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
@@ -31,7 +31,6 @@
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
-import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
import io.supertokens.useridmapping.UserIdMapping;
import io.supertokens.useridmapping.UserIdType;
import io.supertokens.utils.SemVer;
@@ -57,6 +56,7 @@ public String getPath() {
return "/recipe/user";
}
+ @Deprecated
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is tenant specific for get by Email and app specific for get by UserId
@@ -75,7 +75,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
try {
// API is app specific for get by UserId
- UserInfo user = null;
+ AuthRecipeUserInfo user = null;
try {
if (userId != null) {
@@ -89,27 +89,22 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
user = EmailPassword.getUserUsingId(
appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, userId);
-
- // if the userIdMapping exists set the userId in the response to the externalUserId
- if (user != null && appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) {
- user.id = appIdentifierWithStorageAndUserIdMapping.userIdMapping.externalUserId;
- }
+ if (user != null) {
+ UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user});
+ }
} else {
// API is tenant specific for get by Email
// Query by email
String normalisedEmail = Utils.normaliseEmail(email);
- TenantIdentifierWithStorage tenantIdentifierWithStorage = this.getTenantIdentifierWithStorageFromRequest(req);
+ TenantIdentifierWithStorage tenantIdentifierWithStorage =
+ this.getTenantIdentifierWithStorageFromRequest(
+ req);
user = EmailPassword.getUserUsingEmail(tenantIdentifierWithStorage, normalisedEmail);
// if a userIdMapping exists, set the userId in the response to the externalUserId
if (user != null) {
- io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping =
- UserIdMapping.getUserIdMapping(
- getAppIdentifierWithStorage(req), user.id, UserIdType.SUPERTOKENS);
- if (userIdMapping != null) {
- user.id = userIdMapping.externalUserId;
- }
+ UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, new AuthRecipeUserInfo[]{user});
}
}
} catch (UnknownUserIdException e) {
@@ -124,7 +119,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
} else {
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject();
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() :
+ user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
@@ -144,7 +141,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is app specific
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
- String userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ String userId;
+
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ } else {
+ userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+ }
String email = InputParser.parseStringOrThrowError(input, "email", true);
String password = InputParser.parseStringOrThrowError(input, "password", true);
@@ -192,6 +195,12 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
JsonObject result = new JsonObject();
result.addProperty("status", "EMAIL_ALREADY_EXISTS_ERROR");
super.sendJsonResponse(200, result, resp);
+ } catch (EmailChangeNotAllowedException e) {
+ Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), Utils.exceptionStacktraceToString(e));
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR");
+ result.addProperty("reason", "New email is associated with another primary user ID");
+ super.sendJsonResponse(200, result, resp);
}
}
}
diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java
index d53c2e7e8..739d182bd 100644
--- a/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java
@@ -21,6 +21,9 @@
import io.supertokens.Main;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.Multitenancy;
+import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException;
+import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithPhoneNumberAlreadyExistsException;
+import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException;
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
@@ -29,6 +32,7 @@
import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException;
import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException;
import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.utils.SemVer;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
@@ -53,7 +57,13 @@ public String getPath() {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
- String userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ String userId;
+
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ } else {
+ userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+ }
// normalize userId
userId = userId.trim();
@@ -99,6 +109,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
JsonObject result = new JsonObject();
result.addProperty("status", "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR");
super.sendJsonResponse(200, result, resp);
+
+ } catch (AnotherPrimaryUserWithEmailAlreadyExistsException | AnotherPrimaryUserWithPhoneNumberAlreadyExistsException |
+ AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException e) {
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "ASSOCIATION_NOT_ALLOWED_ERROR");
+ result.addProperty("reason", e.getMessage());
+ super.sendJsonResponse(200, result, resp);
}
}
}
diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java
index e97f94f64..a37c3a595 100644
--- a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java
+++ b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java
@@ -26,6 +26,7 @@
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.utils.SemVer;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
@@ -49,7 +50,13 @@ public String getPath() {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
- String userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ String userId;
+
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ } else {
+ userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+ }
// normalize userId
userId = userId.trim();
diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java
index 5e654c22a..6329a81d2 100644
--- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java
@@ -16,9 +16,7 @@
package io.supertokens.webserver.api.passwordless;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import io.supertokens.ActiveUsers;
import io.supertokens.Main;
import io.supertokens.multitenancy.exception.BadPermissionException;
@@ -26,6 +24,8 @@
import io.supertokens.passwordless.Passwordless.ConsumeCodeResponse;
import io.supertokens.passwordless.exceptions.*;
import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
@@ -41,6 +41,7 @@
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
public class ConsumeCodeAPI extends WebserverAPI {
@@ -85,20 +86,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode(
this.getTenantIdentifierWithStorageFromRequest(req), main,
deviceId, deviceIdHash,
- userInputCode, linkCode);
-
- ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.id);
-
- UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
- this.getAppIdentifierWithStorage(req),
- consumeCodeResponse.user.id, UserIdType.ANY);
- if (userIdMapping != null) {
- consumeCodeResponse.user.id = userIdMapping.externalUserId;
- }
+ userInputCode, linkCode,
+ // From CDI version 4.0 onwards, the email verification will be set
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0));
+ io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user});
+
+ ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.getSupertokensUserId());
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(consumeCodeResponse.user)).getAsJsonObject();
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? consumeCodeResponse.user.toJson() :
+ consumeCodeResponse.user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
@@ -106,6 +105,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
result.addProperty("createdNewUser", consumeCodeResponse.createdNewUser);
result.add("user", userJson);
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ for (LoginMethod loginMethod : consumeCodeResponse.user.loginMethods) {
+ if (loginMethod.recipeId.equals(RECIPE_ID.PASSWORDLESS)
+ && (consumeCodeResponse.email == null || Objects.equals(loginMethod.email, consumeCodeResponse.email))
+ && (consumeCodeResponse.phoneNumber == null || Objects.equals(loginMethod.phoneNumber, consumeCodeResponse.phoneNumber))) {
+ result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId());
+ break;
+ }
+ }
+ }
super.sendJsonResponse(200, result, resp);
} catch (RestartFlowException ex) {
@@ -127,7 +136,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
super.sendJsonResponse(200, result, resp);
} catch (DeviceIdHashMismatchException ex) {
throw new ServletException(new BadRequestException("preAuthSessionId and deviceId doesn't match"));
- } catch (StorageTransactionLogicException | StorageQueryException | NoSuchAlgorithmException | InvalidKeyException | TenantOrAppNotFoundException | BadPermissionException e) {
+ } catch (StorageTransactionLogicException | StorageQueryException | NoSuchAlgorithmException |
+ InvalidKeyException | TenantOrAppNotFoundException | BadPermissionException e) {
throw new ServletException(e);
} catch (Base64EncodingException ex) {
throw new ServletException(new BadRequestException("Input encoding error in " + ex.source));
diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java
index ccd7a7651..253a3f81a 100644
--- a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java
@@ -16,22 +16,21 @@
package io.supertokens.webserver.api.passwordless;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
import io.supertokens.Main;
-import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
import io.supertokens.passwordless.Passwordless;
import io.supertokens.passwordless.Passwordless.FieldUpdate;
+import io.supertokens.passwordless.exceptions.PhoneNumberChangeNotAllowedException;
import io.supertokens.passwordless.exceptions.UserWithoutContactInfoException;
import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
-import io.supertokens.pluginInterface.passwordless.UserInfo;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException;
-import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.useridmapping.UserIdType;
import io.supertokens.utils.SemVer;
import io.supertokens.utils.Utils;
@@ -58,6 +57,7 @@ public String getPath() {
return "/recipe/user";
}
+ @Deprecated
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is tenant specific for get by email or phone
@@ -73,7 +73,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
}
try {
- UserInfo user;
+ AuthRecipeUserInfo user;
if (userId != null) {
try {
AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping =
@@ -85,8 +85,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
userId);
// if the userIdMapping exists set the userId in the response to the externalUserId
- if (user != null && appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) {
- user.id = appIdentifierWithStorageAndUserIdMapping.userIdMapping.externalUserId;
+ if (user != null) {
+ io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user});
}
} catch (UnknownUserIdException e) {
user = null;
@@ -95,22 +95,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
email = Utils.normaliseEmail(email);
user = Passwordless.getUserByEmail(this.getTenantIdentifierWithStorageFromRequest(req), email);
if (user != null) {
- UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
- this.getAppIdentifierWithStorage(req),
- user.id, UserIdType.SUPERTOKENS);
- if (userIdMapping != null) {
- user.id = userIdMapping.externalUserId;
- }
+ io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{user});
}
} else {
- user = Passwordless.getUserByPhoneNumber(this.getTenantIdentifierWithStorageFromRequest(req), phoneNumber);
+ user = Passwordless.getUserByPhoneNumber(this.getTenantIdentifierWithStorageFromRequest(req),
+ phoneNumber);
if (user != null) {
- UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
- this.getAppIdentifierWithStorage(req),
- user.id, UserIdType.SUPERTOKENS);
- if (userIdMapping != null) {
- user.id = userIdMapping.externalUserId;
- }
+ io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{user});
}
}
@@ -123,7 +114,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject();
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() :
+ user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
@@ -142,15 +135,21 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
// API is app specific
// logic based on: https://app.code2flow.com/TXloWHJOwWKg
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
- String userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ String userId;
+
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ userId = InputParser.parseStringOrThrowError(input, "userId", false);
+ } else {
+ userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false);
+ }
FieldUpdate emailUpdate = !input.has("email") ? null
: new FieldUpdate(input.get("email").isJsonNull() ? null
- : Utils.normaliseEmail(InputParser.parseStringOrThrowError(input, "email", false)));
+ : Utils.normaliseEmail(InputParser.parseStringOrThrowError(input, "email", false)));
FieldUpdate phoneNumberUpdate = !input.has("phoneNumber") ? null
: new FieldUpdate(input.get("phoneNumber").isJsonNull() ? null
- : InputParser.parseStringOrThrowError(input, "phoneNumber", false));
+ : InputParser.parseStringOrThrowError(input, "phoneNumber", false));
try {
AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping =
@@ -183,6 +182,16 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO
} catch (UserWithoutContactInfoException e) {
throw new ServletException(
new BadRequestException("You cannot clear both email and phone number of a user"));
+ } catch (EmailChangeNotAllowedException e) {
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR");
+ result.addProperty("reason", "New email is associated with another primary user ID");
+ super.sendJsonResponse(200, result, resp);
+ } catch (PhoneNumberChangeNotAllowedException e) {
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR");
+ result.addProperty("reason", "New phone number is associated with another primary user ID");
+ super.sendJsonResponse(200, result, resp);
}
}
}
diff --git a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java
index 33e92983f..ee92da0bd 100644
--- a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java
@@ -109,24 +109,31 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
if (version.lesserThan(SemVer.v3_0)) {
result.get("session").getAsJsonObject().remove("tenantId");
}
+ if (version.lesserThan(SemVer.v4_0)) {
+ result.get("session").getAsJsonObject().remove("recipeUserId");
+ }
result.addProperty("status", "OK");
super.sendJsonResponse(200, result, resp);
- } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) {
+ } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException |
+ UnsupportedJWTSigningAlgorithmException e) {
throw new ServletException(e);
} catch (AccessTokenPayloadError | UnauthorisedException e) {
- Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(), Utils.exceptionStacktraceToString(e));
+ Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(),
+ Utils.exceptionStacktraceToString(e));
JsonObject reply = new JsonObject();
reply.addProperty("status", "UNAUTHORISED");
reply.addProperty("message", e.getMessage());
super.sendJsonResponse(200, reply, resp);
} catch (TokenTheftDetectedException e) {
- Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(), Utils.exceptionStacktraceToString(e));
+ Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(),
+ Utils.exceptionStacktraceToString(e));
JsonObject reply = new JsonObject();
reply.addProperty("status", "TOKEN_THEFT_DETECTED");
JsonObject session = new JsonObject();
session.addProperty("handle", e.sessionHandle);
- session.addProperty("userId", e.userId);
+ session.addProperty("userId", e.primaryUserId);
+ session.addProperty("recipeUserId", e.recipeUserId);
reply.add("session", session);
super.sendJsonResponse(200, reply, resp);
diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java
index 0ea0b0be9..4263285c2 100644
--- a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java
@@ -34,7 +34,6 @@
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.session.SessionInfo;
-import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.session.Session;
import io.supertokens.session.accessToken.AccessToken;
import io.supertokens.session.info.SessionInformationHolder;
@@ -108,8 +107,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
try {
io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping =
io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
- this.getAppIdentifierWithStorage(req),
- sessionInfo.session.userId, UserIdType.ANY);
+ this.getAppIdentifierWithStorage(req),
+ sessionInfo.session.userId, UserIdType.ANY);
if (userIdMapping != null) {
ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main,
userIdMapping.superTokensUserId);
@@ -126,6 +125,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
result.get("session").getAsJsonObject().remove("tenantId");
}
+ if (version.lesserThan(SemVer.v4_0)) {
+ result.get("session").getAsJsonObject().remove("recipeUserId");
+ }
result.addProperty("status", "OK");
@@ -139,7 +141,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
super.sendJsonResponse(200, result, resp);
} catch (AccessTokenPayloadError e) {
throw new ServletException(new BadRequestException(e.getMessage()));
- } catch (NoSuchAlgorithmException | StorageQueryException | InvalidKeyException | InvalidKeySpecException | StorageTransactionLogicException | SignatureException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | NoSuchPaddingException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) {
+ } catch (NoSuchAlgorithmException | StorageQueryException | InvalidKeyException | InvalidKeySpecException |
+ StorageTransactionLogicException | SignatureException | IllegalBlockSizeException |
+ BadPaddingException | InvalidAlgorithmParameterException | NoSuchPaddingException |
+ TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) {
throw new ServletException(e);
}
}
@@ -153,7 +158,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
TenantIdentifierWithStorage tenantIdentifierWithStorage = null;
try {
AppIdentifierWithStorage appIdentifier = getAppIdentifierWithStorage(req);
- TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), Session.getTenantIdFromSessionHandle(sessionHandle));
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
+ appIdentifier.getAppId(), Session.getTenantIdFromSessionHandle(sessionHandle));
tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
} catch (TenantOrAppNotFoundException e) {
throw new ServletException(e);
@@ -171,6 +177,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v3_0)) {
result.addProperty("tenantId", tenantIdentifierWithStorage.getTenantId());
}
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ result.remove("recipeUserId");
+ }
super.sendJsonResponse(200, result, resp);
diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java
index 471518e3e..0f2843e65 100644
--- a/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java
@@ -86,6 +86,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
result.get("session").getAsJsonObject().remove("tenantId");
}
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ result.get("session").getAsJsonObject().remove("recipeUserId");
+ }
result.addProperty("status", "OK");
super.sendJsonResponse(200, result, resp);
diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java
index 8446e20a9..127e0b5fc 100644
--- a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java
@@ -80,22 +80,33 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
Boolean revokeAcrossAllTenants = InputParser.parseBooleanOrThrowError(input, "revokeAcrossAllTenants", true);
if (userId == null && revokeAcrossAllTenants != null) {
- throw new ServletException(new BadRequestException("Invalid JSON input - revokeAcrossAllTenants can only be set if userId is set"));
+ throw new ServletException(new BadRequestException(
+ "Invalid JSON input - revokeAcrossAllTenants can only be set if userId is set"));
}
-
if (revokeAcrossAllTenants == null) {
revokeAcrossAllTenants = true;
}
+ Boolean revokeSessionsForLinkedAccounts = InputParser.parseBooleanOrThrowError(input,
+ "revokeSessionsForLinkedAccounts", true);
+ if (userId == null && revokeSessionsForLinkedAccounts != null) {
+ throw new ServletException(new BadRequestException(
+ "Invalid JSON input - revokeSessionsForLinkedAccounts can only be set if userId is set"));
+ }
+ if (revokeSessionsForLinkedAccounts == null) {
+ revokeSessionsForLinkedAccounts = true;
+ }
+
if (userId != null) {
try {
String[] sessionHandlesRevoked;
if (revokeAcrossAllTenants) {
sessionHandlesRevoked = Session.revokeAllSessionsForUser(
- main, this.getAppIdentifierWithStorage(req), userId);
+ main, this.getAppIdentifierWithStorage(req), userId, revokeSessionsForLinkedAccounts);
} else {
sessionHandlesRevoked = Session.revokeAllSessionsForUser(
- main, this.getTenantIdentifierWithStorageFromRequest(req), userId);
+ main, this.getTenantIdentifierWithStorageFromRequest(req), userId,
+ revokeSessionsForLinkedAccounts);
}
if (StorageLayer.getStorage(this.getTenantIdentifierWithStorageFromRequest(req), main).getType() ==
diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java
index 443594c7c..e31c32ee1 100644
--- a/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java
@@ -20,9 +20,9 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.supertokens.Main;
-import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.session.Session;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
@@ -58,14 +58,21 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
fetchAcrossAllTenants = fetchAcrossAllTenantsString.toLowerCase().equals("true");
}
+ String fetchSessionsForAllLinkedAccountsString = InputParser.getQueryParamOrThrowError(req,
+ "fetchSessionsForAllLinkedAccounts", true);
+ boolean fetchSessionsForAllLinkedAccounts = true;
+ if (fetchSessionsForAllLinkedAccountsString != null) {
+ fetchSessionsForAllLinkedAccounts = fetchSessionsForAllLinkedAccountsString.toLowerCase().equals("true");
+ }
+
try {
String[] sessionHandles;
if (fetchAcrossAllTenants) {
sessionHandles = Session.getAllNonExpiredSessionHandlesForUser(
- main, this.getAppIdentifierWithStorage(req), userId);
+ main, this.getAppIdentifierWithStorage(req), userId, fetchSessionsForAllLinkedAccounts);
} else {
sessionHandles = Session.getAllNonExpiredSessionHandlesForUser(
- this.getTenantIdentifierWithStorageFromRequest(req), userId);
+ this.getTenantIdentifierWithStorageFromRequest(req), userId, fetchSessionsForAllLinkedAccounts);
}
JsonObject result = new JsonObject();
diff --git a/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java
index a1da647bc..8b3fd7d84 100644
--- a/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java
@@ -95,7 +95,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
if (!super.getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v2_21)) {
result.addProperty("jwtSigningPublicKey",
- new Utils.PubPriKey(SigningKeys.getInstance(appIdentifier, main).getLatestIssuedDynamicKey().value).publicKey);
+ new Utils.PubPriKey(SigningKeys.getInstance(appIdentifier, main)
+ .getLatestIssuedDynamicKey().value).publicKey);
result.addProperty("jwtSigningPublicKeyExpiryTime",
SigningKeys.getInstance(appIdentifier, main).getDynamicSigningKeyExpiryTime());
@@ -106,9 +107,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
result.get("session").getAsJsonObject().remove("tenantId");
}
+ if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) {
+ result.get("session").getAsJsonObject().remove("recipeUserId");
+ }
super.sendJsonResponse(200, result, resp);
- } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) {
+ } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException |
+ UnsupportedJWTSigningAlgorithmException e) {
throw new ServletException(e);
} catch (AccessTokenPayloadError e) {
throw new ServletException(new BadRequestException(e.getMessage()));
@@ -136,7 +141,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
reply.addProperty("message", e.getMessage());
super.sendJsonResponse(200, reply, resp);
- } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e2) {
+ } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException |
+ UnsupportedJWTSigningAlgorithmException e2) {
throw new ServletException(e2);
}
}
diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java
index 73350b50a..391db2c70 100644
--- a/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java
@@ -16,14 +16,16 @@
package io.supertokens.webserver.api.thirdparty;
-import com.google.gson.*;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import io.supertokens.Main;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
-import io.supertokens.pluginInterface.RECIPE_ID;
-import io.supertokens.pluginInterface.exceptions.StorageQueryException;
-import io.supertokens.pluginInterface.thirdparty.UserInfo;
import io.supertokens.thirdparty.ThirdParty;
import io.supertokens.useridmapping.UserIdMapping;
import io.supertokens.useridmapping.UserIdType;
@@ -37,6 +39,7 @@
import java.io.IOException;
+@Deprecated
public class GetUsersByEmailAPI extends WebserverAPI {
private static final long serialVersionUID = -4413719941975228004L;
@@ -53,27 +56,22 @@ public String getPath() {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// this API is tenant specific
try {
- TenantIdentifierWithStorage tenantIdentifierWithStorage = this.getTenantIdentifierWithStorageFromRequest(req);
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = this.getTenantIdentifierWithStorageFromRequest(
+ req);
AppIdentifierWithStorage appIdentifierWithStorage = this.getAppIdentifierWithStorage(req);
String email = InputParser.getQueryParamOrThrowError(req, "email", false);
email = Utils.normaliseEmail(email);
- UserInfo[] users = ThirdParty.getUsersByEmail(tenantIdentifierWithStorage, email);
-
- // return the externalUserId if a mapping exists for a user
- for (int i = 0; i < users.length; i++) {
- // we intentionally do not use the function that accepts an array of user IDs to get the mapping cause
- // this is simpler to use, and cause there shouldn't be that many userIds per email anyway
- io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping
- .getUserIdMapping(appIdentifierWithStorage, users[i].id, UserIdType.SUPERTOKENS);
- if (userIdMapping != null) {
- users[i].id = userIdMapping.externalUserId;
- }
- }
+ AuthRecipeUserInfo[] users = ThirdParty.getUsersByEmail(tenantIdentifierWithStorage, email);
+ UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, users);
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonArray usersJson = new JsonParser().parse(new Gson().toJson(users)).getAsJsonArray();
+ JsonArray usersJson = new JsonArray();
+ for (AuthRecipeUserInfo userInfo : users) {
+ usersJson.add(getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? userInfo.toJson() :
+ userInfo.toJsonWithoutAccountLinking());
+ }
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
for (JsonElement user : usersJson) {
diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java
index 61e612278..06042eb23 100644
--- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java
@@ -16,13 +16,14 @@
package io.supertokens.webserver.api.thirdparty;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import io.supertokens.ActiveUsers;
import io.supertokens.Main;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.thirdparty.ThirdParty;
@@ -37,6 +38,7 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.util.Objects;
public class SignInUpAPI extends WebserverAPI {
@@ -77,19 +79,30 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
this.getTenantIdentifierWithStorageFromRequest(req), super.main,
thirdPartyId,
thirdPartyUserId, email, isEmailVerified);
+ UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user});
- ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.id);
+ ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId());
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
result.addProperty("createdNewUser", response.createdNewUser);
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(response.user)).getAsJsonObject();
+ JsonObject userJson = response.user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
}
result.add("user", userJson);
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ for (LoginMethod loginMethod : response.user.loginMethods) {
+ if (loginMethod.recipeId.equals(RECIPE_ID.THIRD_PARTY)
+ && Objects.equals(loginMethod.thirdParty.id, thirdPartyId)
+ && Objects.equals(loginMethod.thirdParty.userId, thirdPartyUserId)) {
+ result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId());
+ break;
+ }
+ }
+ }
super.sendJsonResponse(200, result, resp);
} catch (StorageQueryException | TenantOrAppNotFoundException e) {
@@ -102,6 +115,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
JsonObject emailObject = InputParser.parseJsonObjectOrThrowError(input, "email", false);
String email = InputParser.parseStringOrThrowError(emailObject, "id", false);
+ // setting email verified behaviour is to be done only for CDI 4.0 onwards. version 3.0 and earlier
+ // do not have this field
+ Boolean isEmailVerified = false;
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ isEmailVerified = InputParser.parseBooleanOrThrowError(emailObject, "isVerified", false);
+ }
+
assert thirdPartyId != null;
assert thirdPartyUserId != null;
assert email != null;
@@ -116,32 +136,42 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
try {
ThirdParty.SignInUpResponse response = ThirdParty.signInUp(
this.getTenantIdentifierWithStorageFromRequest(req), super.main, thirdPartyId, thirdPartyUserId,
- email);
-
- ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.id);
+ email, isEmailVerified);
+ UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user});
- //
- io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping
- .getUserIdMapping(this.getAppIdentifierWithStorage(req), response.user.id,
- UserIdType.SUPERTOKENS);
- if (userIdMapping != null) {
- response.user.id = userIdMapping.externalUserId;
- }
+ ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId());
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
result.addProperty("createdNewUser", response.createdNewUser);
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(response.user)).getAsJsonObject();
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? response.user.toJson() :
+ response.user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
}
result.add("user", userJson);
+ if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) {
+ for (LoginMethod loginMethod : response.user.loginMethods) {
+ if (loginMethod.recipeId.equals(RECIPE_ID.THIRD_PARTY)
+ && Objects.equals(loginMethod.thirdParty.id, thirdPartyId)
+ && Objects.equals(loginMethod.thirdParty.userId, thirdPartyUserId)) {
+ result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId());
+ break;
+ }
+ }
+ }
super.sendJsonResponse(200, result, resp);
} catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) {
throw new ServletException(e);
+ } catch (EmailChangeNotAllowedException e) {
+ JsonObject result = new JsonObject();
+ result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR");
+ result.addProperty("reason", "Email already associated with another primary user.");
+ super.sendJsonResponse(200, result, resp);
}
}
diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java
index 9fa23bdfb..2ce566281 100644
--- a/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java
+++ b/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java
@@ -16,16 +16,14 @@
package io.supertokens.webserver.api.thirdparty;
-import com.google.gson.Gson;
import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import io.supertokens.AppIdentifierWithStorageAndUserIdMapping;
import io.supertokens.Main;
-import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
-import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
-import io.supertokens.pluginInterface.thirdparty.UserInfo;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.thirdparty.ThirdParty;
import io.supertokens.useridmapping.UserIdMapping;
import io.supertokens.useridmapping.UserIdType;
@@ -51,6 +49,7 @@ public String getPath() {
return "/recipe/user";
}
+ @Deprecated
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is tenant specific for get by thirdPartyUserId
@@ -72,7 +71,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
}
try {
- UserInfo user = null;
+ AuthRecipeUserInfo user = null;
if (userId != null) {
// Query by userId
try {
@@ -83,9 +82,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
userId = appIdentifierWithStorageAndUserIdMapping.userIdMapping.superTokensUserId;
}
- user = ThirdParty.getUser(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, userId);
- if (user != null && appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) {
- user.id = appIdentifierWithStorageAndUserIdMapping.userIdMapping.externalUserId;
+ user = ThirdParty.getUser(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage,
+ userId);
+ if (user != null) {
+ UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user});
}
} catch (UnknownUserIdException e) {
// let the user be null
@@ -94,11 +94,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
user = ThirdParty.getUser(this.getTenantIdentifierWithStorageFromRequest(req), thirdPartyId,
thirdPartyUserId);
if (user != null) {
- io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping
- .getUserIdMapping(this.getAppIdentifierWithStorage(req), user.id, UserIdType.SUPERTOKENS);
- if (userIdMapping != null) {
- user.id = userIdMapping.externalUserId;
- }
+ UserIdMapping.populateExternalUserIdForUsers(getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{user});
}
}
@@ -110,7 +106,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
} else {
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
- JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject();
+ JsonObject userJson =
+ getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() :
+ user.toJsonWithoutAccountLinking();
if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) {
userJson.remove("tenantIds");
diff --git a/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java b/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java
index acb2f0407..e8209e53d 100644
--- a/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java
+++ b/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java
@@ -20,7 +20,7 @@
import io.supertokens.ProcessState;
import io.supertokens.emailpassword.EmailPassword;
import io.supertokens.pluginInterface.STORAGE_TYPE;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.httpRequest.HttpRequestForTesting;
import io.supertokens.test.httpRequest.HttpResponseException;
@@ -67,11 +67,11 @@ public void deleteUser() throws Exception {
assertEquals(response.entrySet().size(), 1);
}
- UserInfo user = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0");
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0");
{
JsonObject requestBody = new JsonObject();
- requestBody.addProperty("userId", user.id);
+ requestBody.addProperty("userId", user.getSupertokensUserId());
JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/user/remove", requestBody, 1000, 1000, null,
@@ -80,7 +80,7 @@ public void deleteUser() throws Exception {
assertEquals(response.entrySet().size(), 1);
}
- assertNull(EmailPassword.getUserUsingId(process.getProcess(), user.id));
+ assertNull(EmailPassword.getUserUsingId(process.getProcess(), user.getSupertokensUserId()));
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
diff --git a/src/test/java/io/supertokens/test/AuthRecipeTest.java b/src/test/java/io/supertokens/test/AuthRecipeTest.java
index 42e0ce0db..d941f81cc 100644
--- a/src/test/java/io/supertokens/test/AuthRecipeTest.java
+++ b/src/test/java/io/supertokens/test/AuthRecipeTest.java
@@ -28,7 +28,6 @@
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
-import io.supertokens.pluginInterface.emailpassword.UserInfo;
import io.supertokens.session.Session;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.thirdparty.ThirdParty;
@@ -66,7 +65,7 @@ public void beforeEach() {
@Test
public void getUsersCount() throws Exception {
- String[] args = { "../" };
+ String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
@@ -89,17 +88,17 @@ public void getUsersCount() throws Exception {
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD});
assert (count == 2);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY});
assert (count == 0);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.PASSWORDLESS });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.PASSWORDLESS});
assert (count == 0);
}
@@ -110,28 +109,28 @@ public void getUsersCount() throws Exception {
ThirdParty.signInUp(process.getProcess(), thirdPartyId, thirdPartyUserId_1, email_1);
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] {});
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{});
assert (count == 3);
}
{
long count = AuthRecipe.getUsersCount(process.getProcess(),
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY });
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY});
assert (count == 3);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD});
assert (count == 2);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY});
assert (count == 1);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.PASSWORDLESS });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.PASSWORDLESS});
assert (count == 0);
}
@@ -147,28 +146,28 @@ public void getUsersCount() throws Exception {
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] {});
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{});
assert (count == 5);
}
{
long count = AuthRecipe.getUsersCount(process.getProcess(),
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.PASSWORDLESS });
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.PASSWORDLESS});
assert (count == 4);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD});
assert (count == 2);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY});
assert (count == 1);
}
{
- long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.PASSWORDLESS });
+ long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.PASSWORDLESS});
assert (count == 2);
}
process.kill();
@@ -177,7 +176,7 @@ public void getUsersCount() throws Exception {
@Test
public void paginationTest() throws Exception {
- String[] args = { "../" };
+ String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
@@ -194,107 +193,107 @@ public void paginationTest() throws Exception {
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 0);
}
- UserInfo user1 = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0");
- UserInfo user2 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1");
- UserInfo user3 = EmailPassword.signUp(process.getProcess(), "test20@example.com", "password2");
- UserInfo user4 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password3");
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0");
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1");
+ AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test20@example.com", "password2");
+ AuthRecipeUserInfo user4 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password3");
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 4);
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user1));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user4));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user1));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 4);
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user1));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user4));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user1));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 4);
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user1));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user2));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user3));
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user4));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user1));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user2));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user3));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 4);
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user1));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user2));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user3));
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user4));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user1));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user2));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user3));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.SESSION }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.SESSION}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 0);
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 2, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken != null);
assert (users.users.length == 2);
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user3));
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user4));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user3));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 3, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken != null);
assert (users.users.length == 3);
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user1));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user1));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
}
///////////////////////////////////////////////////////////////////////////////////
@@ -303,124 +302,124 @@ public void paginationTest() throws Exception {
String thirdPartyUserId_1 = "thirdPartyUserIdA";
String email_1 = "testA@example.com";
- io.supertokens.pluginInterface.thirdparty.UserInfo user5 = ThirdParty.signInUp(process.getProcess(),
+ AuthRecipeUserInfo user5 = ThirdParty.signInUp(process.getProcess(),
thirdPartyId, thirdPartyUserId_1, email_1).user;
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 4);
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user1));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user4));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user1));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 1);
- assert (users.users[0].recipeId.equals("thirdparty"));
- assert (users.users[0].user.equals(user5));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty"));
+ assert (users.users[0].equals(user5));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 5);
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user1));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user4));
- assert (users.users[4].recipeId.equals("thirdparty"));
- assert (users.users[4].user.equals(user5));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user1));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user4));
+ assert (users.users[4].loginMethods[0].recipeId.toString().equals("thirdparty"));
+ assert (users.users[4].equals(user5));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 4);
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user1));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user2));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user3));
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user4));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user1));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user2));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user3));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user4));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 1);
- assert (users.users[0].recipeId.equals("thirdparty"));
- assert (users.users[0].user.equals(user5));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty"));
+ assert (users.users[0].equals(user5));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] {}, null);
+ new RECIPE_ID[]{}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 5);
- assert (users.users[4].recipeId.equals("emailpassword"));
- assert (users.users[4].user.equals(user1));
- assert (users.users[3].recipeId.equals("emailpassword"));
- assert (users.users[3].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user4));
- assert (users.users[0].recipeId.equals("thirdparty"));
- assert (users.users[0].user.equals(user5));
+ assert (users.users[4].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[4].equals(user1));
+ assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[3].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user4));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty"));
+ assert (users.users[0].equals(user5));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.SESSION }, null);
+ new RECIPE_ID[]{RECIPE_ID.SESSION}, null);
assert (users.nextPaginationToken == null);
assert (users.users.length == 0);
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 2, "DESC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken != null);
assert (users.users.length == 2);
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user4));
- assert (users.users[0].recipeId.equals("thirdparty"));
- assert (users.users[0].user.equals(user5));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user4));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty"));
+ assert (users.users[0].equals(user5));
}
{
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 3, "ASC", null,
- new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null);
+ new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null);
assert (users.nextPaginationToken != null);
assert (users.users.length == 3);
- assert (users.users[0].recipeId.equals("emailpassword"));
- assert (users.users[0].user.equals(user1));
- assert (users.users[1].recipeId.equals("emailpassword"));
- assert (users.users[1].user.equals(user2));
- assert (users.users[2].recipeId.equals("emailpassword"));
- assert (users.users[2].user.equals(user3));
+ assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[0].equals(user1));
+ assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[1].equals(user2));
+ assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword"));
+ assert (users.users[2].equals(user3));
}
process.kill();
@@ -430,8 +429,8 @@ public void paginationTest() throws Exception {
@Test
public void randomPaginationTest() throws Exception {
int numberOfUsers = 500;
- int[] limits = new int[] { 10, 14, 20, 23, 50, 100, 110, 150, 200, 510 };
- String[] args = { "../" };
+ int[] limits = new int[]{10, 14, 20, 23, 50, 100, 110, 150, 200, 510};
+ String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
@@ -440,11 +439,11 @@ public void randomPaginationTest() throws Exception {
return;
}
- Map> signUpMap = getSignUpMap(process);
+ Map> signUpMap = getSignUpMap(process);
- List classes = getUserInfoClassNameList();
+ List authRecipes = getAuthRecipes();
- for (String className : classes) {
+ for (String className : authRecipes) {
if (!signUpMap.containsKey(className)) {
fail();
}
@@ -457,7 +456,7 @@ public void randomPaginationTest() throws Exception {
for (int i = 0; i < numberOfUsers; i++) {
if (Math.random() > 0.5) {
while (true) {
- String currUserType = classes.get((int) (Math.random() * classes.size()));
+ String currUserType = authRecipes.get((int) (Math.random() * authRecipes.size()));
AuthRecipeUserInfo user = signUpMap.get(currUserType).apply(null);
if (user != null) {
synchronized (usersCreated) {
@@ -469,7 +468,7 @@ public void randomPaginationTest() throws Exception {
} else {
es.execute(() -> {
while (true) {
- String currUserType = classes.get((int) (Math.random() * classes.size()));
+ String currUserType = authRecipes.get((int) (Math.random() * authRecipes.size()));
AuthRecipeUserInfo user = signUpMap.get(currUserType).apply(null);
if (user != null) {
synchronized (usersCreated) {
@@ -497,7 +496,7 @@ public void randomPaginationTest() throws Exception {
if (o1.timeJoined != o2.timeJoined) {
return (int) (o1.timeJoined - o2.timeJoined);
}
- return o2.id.compareTo(o1.id);
+ return o2.getSupertokensUserId().compareTo(o1.getSupertokensUserId());
});
// we make sure it's sorted properly..
@@ -516,11 +515,11 @@ public void randomPaginationTest() throws Exception {
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), limit, "ASC",
paginationToken, null, null);
- for (UserPaginationContainer.UsersContainer uc : users.users) {
+ for (AuthRecipeUserInfo uc : users.users) {
AuthRecipeUserInfo expected = usersCreated.get(indexIntoUsers);
- AuthRecipeUserInfo actualUser = uc.user;
+ AuthRecipeUserInfo actualUser = uc;
- assert (actualUser.equals(expected) && uc.recipeId.equals(expected.getRecipeId().toString()));
+ assert (actualUser.equals(expected) && uc.loginMethods[0].recipeId.toString().equals(expected.loginMethods[0].recipeId.toString()));
indexIntoUsers++;
}
@@ -537,7 +536,7 @@ public void randomPaginationTest() throws Exception {
if (o1.timeJoined != o2.timeJoined) {
return (int) (o1.timeJoined - o2.timeJoined);
}
- return o1.id.compareTo(o2.id);
+ return o1.getSupertokensUserId().compareTo(o2.getSupertokensUserId());
});
// we make sure it's sorted properly..
@@ -555,11 +554,11 @@ public void randomPaginationTest() throws Exception {
UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), limit, "DESC",
paginationToken, null, null);
- for (UserPaginationContainer.UsersContainer uc : users.users) {
+ for (AuthRecipeUserInfo uc : users.users) {
AuthRecipeUserInfo expected = usersCreated.get(indexIntoUsers);
- AuthRecipeUserInfo actualUser = uc.user;
+ AuthRecipeUserInfo actualUser = uc;
- assert (actualUser.equals(expected) && uc.recipeId.equals(expected.getRecipeId().toString()));
+ assert (actualUser.equals(expected) && uc.loginMethods[0].recipeId.toString().equals(expected.loginMethods[0].recipeId.toString()));
indexIntoUsers--;
}
@@ -576,7 +575,7 @@ public void randomPaginationTest() throws Exception {
@Test
public void deleteUserTest() throws Exception {
- String[] args = { "../" };
+ String[] args = {"../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
@@ -585,43 +584,43 @@ public void deleteUserTest() throws Exception {
return;
}
- Map> signUpMap = getSignUpMap(process);
+ Map> signUpMap = getSignUpMap(process);
- List classes = getUserInfoClassNameList();
+ List authRecipes = getAuthRecipes();
- for (String className : classes) {
+ for (String className : authRecipes) {
if (!signUpMap.containsKey(className)) {
fail();
}
}
- for (String userType : classes) {
+ for (String userType : authRecipes) {
AuthRecipeUserInfo user1 = signUpMap.get(userType).apply(null);
JsonObject testMetadata = new JsonObject();
testMetadata.addProperty("test", "test");
- UserMetadata.updateUserMetadata(process.getProcess(), user1.id, testMetadata);
- Session.createNewSession(process.getProcess(), user1.id, new JsonObject(), new JsonObject());
+ UserMetadata.updateUserMetadata(process.getProcess(), user1.getSupertokensUserId(), testMetadata);
+ Session.createNewSession(process.getProcess(), user1.getSupertokensUserId(), new JsonObject(), new JsonObject());
String emailVerificationToken = EmailVerification.generateEmailVerificationToken(process.getProcess(),
- user1.id, "email");
+ user1.getSupertokensUserId(), "email");
EmailVerification.verifyEmail(process.getProcess(), emailVerificationToken);
AuthRecipeUserInfo user2 = signUpMap.get(userType).apply(null);
- Session.createNewSession(process.getProcess(), user2.id, new JsonObject(), new JsonObject());
+ Session.createNewSession(process.getProcess(), user2.getSupertokensUserId(), new JsonObject(), new JsonObject());
String emailVerificationToken2 = EmailVerification.generateEmailVerificationToken(process.getProcess(),
- user2.id, "email");
+ user2.getSupertokensUserId(), "email");
- assertEquals(2, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { user1.getRecipeId() }));
- AuthRecipe.deleteUser(process.getProcess(), user1.id);
- assertEquals(1, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { user1.getRecipeId() }));
- assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user1.id).length);
- assertEquals(1, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.id).length);
- assertFalse(EmailVerification.isEmailVerified(process.getProcess(), user1.id, "email"));
- assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user1.id).entrySet().size());
+ assertEquals(2, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{user1.loginMethods[0].recipeId}));
+ AuthRecipe.deleteUser(process.getProcess(), user1.getSupertokensUserId());
+ assertEquals(1, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{user1.loginMethods[0].recipeId}));
+ assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user1.getSupertokensUserId()).length);
+ assertEquals(1, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.getSupertokensUserId()).length);
+ assertFalse(EmailVerification.isEmailVerified(process.getProcess(), user1.getSupertokensUserId(), "email"));
+ assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user1.getSupertokensUserId()).entrySet().size());
- AuthRecipe.deleteUser(process.getProcess(), user2.id);
- assertEquals(0, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { user1.getRecipeId() }));
- assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.id).length);
- assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user2.id).entrySet().size());
+ AuthRecipe.deleteUser(process.getProcess(), user2.getSupertokensUserId());
+ assertEquals(0, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{user1.loginMethods[0].recipeId}));
+ assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.getSupertokensUserId()).length);
+ assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user2.getSupertokensUserId()).entrySet().size());
Exception error = null;
try {
@@ -636,19 +635,16 @@ public void deleteUserTest() throws Exception {
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}
- private static List getUserInfoClassNameList() {
- Reflections reflections = new Reflections("io.supertokens");
- Set> classes = reflections.getSubTypesOf(AuthRecipeUserInfo.class);
-
- return classes.stream().map(Class::getCanonicalName).collect(Collectors.toList());
+ private static List getAuthRecipes() {
+ return Arrays.asList("emailpassword", "thirdparty", "passwordless");
}
- private static Map> getSignUpMap(
+ private static Map> getSignUpMap(
TestingProcessManager.TestingProcess process) {
AtomicInteger count = new AtomicInteger();
- Map> signUpMap = new HashMap<>();
- signUpMap.put("io.supertokens.pluginInterface.emailpassword.UserInfo", o -> {
+ Map> signUpMap = new HashMap<>();
+ signUpMap.put("emailpassword", o -> {
try {
return EmailPassword.signUp(process.getProcess(), "test" + count.getAndIncrement() + "@example.com",
"password0");
@@ -656,7 +652,7 @@ private static List getUserInfoClassNameList() {
}
return null;
});
- signUpMap.put("io.supertokens.pluginInterface.thirdparty.UserInfo", o -> {
+ signUpMap.put("thirdparty", o -> {
try {
String thirdPartyId = "testThirdParty";
String thirdPartyUserId = "thirdPartyUserId" + count.getAndIncrement();
@@ -667,7 +663,7 @@ private static List getUserInfoClassNameList() {
}
return null;
});
- signUpMap.put("io.supertokens.pluginInterface.passwordless.UserInfo", o -> {
+ signUpMap.put("passwordless", o -> {
try {
String email = "test" + count.getAndIncrement() + "@example.com";
CreateCodeResponse createCode = Passwordless.createCode(process.getProcess(), email, null, null, null);
diff --git a/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java
new file mode 100644
index 000000000..3e65fc432
--- /dev/null
+++ b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 io.supertokens.test;
+
+import io.supertokens.ProcessState;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
+import io.supertokens.emailpassword.exceptions.WrongCredentialsException;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.ThirdParty;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class AuthRecipesParallelTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void timeTakenFor500SignInParallel() throws Exception {
+ String[] args = {"../"};
+
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ ExecutorService ex = Executors.newFixedThreadPool(1000);
+ int numberOfThreads = 500;
+
+ EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ AtomicInteger counter = new AtomicInteger(0);
+ AtomicInteger retryCounter = new AtomicInteger(0);
+
+ long st = System.currentTimeMillis();
+ for (int i = 0; i < numberOfThreads; i++) {
+ ex.execute(() -> {
+ while(true) {
+ try {
+ EmailPassword.signIn(process.getProcess(), "test@example.com", "password");
+ counter.incrementAndGet();
+ break;
+ } catch (StorageQueryException e) {
+ retryCounter.incrementAndGet();
+ // continue
+ } catch (WrongCredentialsException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+
+ ex.shutdown();
+
+ ex.awaitTermination(2, TimeUnit.MINUTES);
+ System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms");
+ System.out.println("Retry counter: " + retryCounter.get());
+ assertEquals(counter.get(), numberOfThreads);
+ assertEquals(0, retryCounter.get());
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void timeTakenFor500SignInUpParallel() throws Exception {
+ String[] args = {"../"};
+
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ if (StorageLayer.isInMemDb(process.getProcess())) {
+ return;
+ }
+
+ ExecutorService ex = Executors.newFixedThreadPool(1000);
+ int numberOfThreads = 500;
+
+ ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com");
+ AtomicInteger counter = new AtomicInteger(0);
+ AtomicInteger retryCounter = new AtomicInteger(0);
+
+ ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com");
+
+ long st = System.currentTimeMillis();
+ for (int i = 0; i < numberOfThreads; i++) {
+ ex.execute(() -> {
+ while(true) {
+ try {
+ ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com");
+ counter.incrementAndGet();
+ break;
+ } catch (StorageQueryException e) {
+ retryCounter.incrementAndGet();
+ // continue
+ } catch (EmailChangeNotAllowedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+
+ ex.shutdown();
+
+ ex.awaitTermination(2, TimeUnit.MINUTES);
+ System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms");
+ System.out.println("Retry counter: " + retryCounter.get());
+ assertEquals (counter.get(), numberOfThreads);
+ assertEquals(0, retryCounter.get());
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+}
diff --git a/src/test/java/io/supertokens/test/FeatureFlagTest.java b/src/test/java/io/supertokens/test/FeatureFlagTest.java
index 15be7c36e..c127f666a 100644
--- a/src/test/java/io/supertokens/test/FeatureFlagTest.java
+++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java
@@ -32,7 +32,9 @@
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.featureflag.exceptions.NoLicenseKeyFoundException;
import io.supertokens.multitenancy.Multitenancy;
+import io.supertokens.multitenancy.MultitenancyHelper;
import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
@@ -509,6 +511,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception {
null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), "");
Assert.assertEquals("OK", response.get("status").getAsString());
+ assertFalse(response.get("usageStats").getAsJsonObject().has("account_linking"));
JsonArray multitenancyStats = response.get("usageStats").getAsJsonObject().get("multi_tenancy")
.getAsJsonObject().get("tenants").getAsJsonArray();
assertEquals(6, multitenancyStats.size());
diff --git a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java
index 4da18541d..1e565cec5 100644
--- a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java
+++ b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java
@@ -186,6 +186,10 @@ public void testCreatingSessionWithAndWithoutAPIKey() throws Exception {
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("key", "value");
@@ -209,7 +213,7 @@ public void testCreatingSessionWithAndWithoutAPIKey() throws Exception {
JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session", request, 1000, 1000, null,
- Utils.getCdiVersionStringLatestForTests(),
+ SemVer.v3_0.get(),
apiKey, "");
assertEquals(sessionInfo.get("status").getAsString(), "OK");
checkSessionResponse(sessionInfo, process, userId, userDataInJWT);
@@ -272,6 +276,10 @@ public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAl
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
String userId = "userId";
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("key", "value");
@@ -296,7 +304,7 @@ public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAl
{
JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session", request, 1000, 1000, null,
- Utils.getCdiVersionStringLatestForTests(),
+ SemVer.v3_0.get(),
apiKey, "");
assertEquals(sessionInfo.get("status").getAsString(), "OK");
checkSessionResponse(sessionInfo, process, userId, userDataInJWT);
@@ -305,7 +313,7 @@ public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAl
{
JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
"http://localhost:3567/recipe/session", request, 1000, 1000, null,
- Utils.getCdiVersionStringLatestForTests(),
+ SemVer.v3_0.get(),
saasSecret, "");
assertEquals(sessionInfo.get("status").getAsString(), "OK");
checkSessionResponse(sessionInfo, process, userId, userDataInJWT);
diff --git a/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java
new file mode 100644
index 000000000..5614de91d
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java
@@ -0,0 +1,549 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
+import io.supertokens.multitenancy.Multitenancy;
+import io.supertokens.passwordless.Passwordless;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.multitenancy.*;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.ThirdParty;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import static org.junit.Assert.assertNotNull;
+
+
+public class CreatePrimaryUserTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void testThatOnSignUpUserIsNotAPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234");
+ assert (!user.isPrimaryUser);
+ assert (user.loginMethods.length == 1);
+ assert (user.loginMethods[0].recipeId == RECIPE_ID.EMAIL_PASSWORD);
+ assert (user.loginMethods[0].email.equals("test@example.com"));
+ assert (user.loginMethods[0].passwordHash != null);
+ assert (user.loginMethods[0].thirdParty == null);
+ assert (user.getSupertokensUserId().equals(user.loginMethods[0].getSupertokensUserId()));
+ assert (user.loginMethods[0].phoneNumber == null);
+
+ ThirdParty.SignInUpResponse resp = ThirdParty.signInUp(process.getProcess(), "google", "user-google",
+ "test@example.com");
+ assert (!resp.user.isPrimaryUser);
+ assert (resp.user.loginMethods.length == 1);
+ assert (resp.user.loginMethods[0].recipeId == RECIPE_ID.THIRD_PARTY);
+ assert (resp.user.loginMethods[0].email.equals("test@example.com"));
+ assert (resp.user.loginMethods[0].thirdParty.userId.equals("user-google"));
+ assert (resp.user.loginMethods[0].thirdParty.id.equals("google"));
+ assert (resp.user.loginMethods[0].phoneNumber == null);
+ assert (resp.user.loginMethods[0].passwordHash == null);
+ assert (resp.user.getSupertokensUserId().equals(resp.user.loginMethods[0].getSupertokensUserId()));
+
+ {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), "u@e.com", null, null,
+ null);
+ Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId,
+ code.deviceIdHash, code.userInputCode, null);
+ assert (!pResp.user.isPrimaryUser);
+ assert (pResp.user.loginMethods.length == 1);
+ assert (pResp.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS);
+ assert (pResp.user.loginMethods[0].email.equals("u@e.com"));
+ assert (pResp.user.loginMethods[0].passwordHash == null);
+ assert (pResp.user.loginMethods[0].thirdParty == null);
+ assert (pResp.user.loginMethods[0].phoneNumber == null);
+ assert (pResp.user.getSupertokensUserId().equals(pResp.user.loginMethods[0].getSupertokensUserId()));
+ }
+
+ {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), null, "12345", null,
+ null);
+ Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId,
+ code.deviceIdHash, code.userInputCode, null);
+ assert (!pResp.user.isPrimaryUser);
+ assert (pResp.user.loginMethods.length == 1);
+ assert (pResp.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS);
+ assert (pResp.user.loginMethods[0].email == null);
+ assert (pResp.user.loginMethods[0].passwordHash == null);
+ assert (pResp.user.loginMethods[0].thirdParty == null);
+ assert (pResp.user.loginMethods[0].phoneNumber.equals("12345"));
+ assert (pResp.user.getSupertokensUserId().equals(pResp.user.loginMethods[0].getSupertokensUserId()));
+ }
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testThatCreationOfPrimaryUserRequiresAccountLinkingFeatureToBeEnabled() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.isInMemDb(process.getProcess())) {
+ // Features are enabled by default for inmemory db
+ return;
+ }
+
+ try {
+ AuthRecipe.createPrimaryUser(process.main,
+ new AppIdentifierWithStorage(null, null, StorageLayer.getStorage(process.main)), "");
+ assert (false);
+ } catch (FeatureNotEnabledException e) {
+ assert (e.getMessage()
+ .equals("Account linking feature is not enabled for this app. Please contact support to enable it" +
+ "."));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makeEmailPasswordPrimaryUserSuccess() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com",
+ "pass1234");
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+ assert (result.user.isPrimaryUser);
+ assert (result.user.loginMethods.length == 1);
+ assert (result.user.loginMethods[0].recipeId == RECIPE_ID.EMAIL_PASSWORD);
+ assert (result.user.loginMethods[0].email.equals("test@example.com"));
+ assert (result.user.loginMethods[0].passwordHash != null);
+ assert (result.user.loginMethods[0].thirdParty == null);
+ assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId()));
+ assert (result.user.loginMethods[0].phoneNumber == null);
+
+ AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId());
+
+ assert (refetchedUser.equals(result.user));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makeThirdPartyPrimaryUserSuccess() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ ThirdParty.SignInUpResponse signInUp = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main,
+ signInUp.user.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+ assert (result.user.isPrimaryUser);
+ assert (result.user.loginMethods.length == 1);
+ assert (result.user.loginMethods[0].recipeId == RECIPE_ID.THIRD_PARTY);
+ assert (result.user.loginMethods[0].email.equals("test@example.com"));
+ assert (result.user.loginMethods[0].thirdParty.userId.equals("user-google"));
+ assert (result.user.loginMethods[0].thirdParty.id.equals("google"));
+ assert (result.user.loginMethods[0].phoneNumber == null);
+ assert (result.user.loginMethods[0].passwordHash == null);
+ assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId()));
+
+ AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId());
+
+ assert (refetchedUser.equals(result.user));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePasswordlessEmailPrimaryUserSuccess() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), "u@e.com", null, null,
+ null);
+ Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId,
+ code.deviceIdHash, code.userInputCode, null);
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main,
+ pResp.user.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+ assert (result.user.isPrimaryUser);
+ assert (result.user.loginMethods.length == 1);
+ assert (result.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS);
+ assert (result.user.loginMethods[0].email.equals("u@e.com"));
+ assert (result.user.loginMethods[0].passwordHash == null);
+ assert (result.user.loginMethods[0].thirdParty == null);
+ assert (result.user.loginMethods[0].phoneNumber == null);
+ assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId()));
+
+ AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId());
+
+ assert (refetchedUser.equals(result.user));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePasswordlessPhonePrimaryUserSuccess() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), null, "1234", null,
+ null);
+ Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId,
+ code.deviceIdHash, code.userInputCode, null);
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main,
+ pResp.user.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+ assert (result.user.isPrimaryUser);
+ assert (result.user.loginMethods.length == 1);
+ assert (result.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS);
+ assert (result.user.loginMethods[0].email == null);
+ assert (result.user.loginMethods[0].passwordHash == null);
+ assert (result.user.loginMethods[0].thirdParty == null);
+ assert (result.user.loginMethods[0].phoneNumber.equals("1234"));
+ assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId()));
+
+ AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId());
+
+ assert (refetchedUser.equals(result.user));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void alreadyPrimaryUsertest() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com",
+ "pass1234");
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+
+ result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId());
+ assert (result.wasAlreadyAPrimaryUser);
+ assert (result.user.getSupertokensUserId().equals(emailPasswordUser.getSupertokensUserId()));
+ assert (result.user.isPrimaryUser);
+ assert (result.user.loginMethods.length == 1);
+ assert (result.user.loginMethods[0].recipeId == RECIPE_ID.EMAIL_PASSWORD);
+ assert (result.user.loginMethods[0].email.equals("test@example.com"));
+ assert (result.user.loginMethods[0].passwordHash != null);
+ assert (result.user.loginMethods[0].thirdParty == null);
+ assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId()));
+ assert (result.user.loginMethods[0].phoneNumber == null);
+
+ AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId());
+
+ assert (refetchedUser.equals(result.user));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com",
+ "pass1234");
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google",
+ "test@example.com");
+
+ try {
+ AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId());
+ assert (false);
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ assert (e.primaryUserId.equals(emailPasswordUser.getSupertokensUserId()));
+ assert (e.getMessage().equals("This user's email is already associated with another user ID"));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePrimarySucceedsEvenIfAnotherAccountWithSameEmailButIsNotAPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com",
+ "pass1234");
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google",
+ "test@example.com");
+
+ AuthRecipe.CreatePrimaryUserResult r = AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId());
+ assert (!r.wasAlreadyAPrimaryUser);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUserInAnotherTenant()
+ throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null),
+ new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true),
+ new JsonObject()));
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1",
+ StorageLayer.getStorage(process.main));
+ AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(),
+ "test@example.com",
+ "pass1234");
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google",
+ "test@example.com");
+
+ Multitenancy.addUserIdToTenant(process.main, tenantIdentifierWithStorage, signInUpResponse.user.getSupertokensUserId());
+
+ try {
+ AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId());
+ assert (false);
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ assert (e.primaryUserId.equals(emailPasswordUser.getSupertokensUserId()));
+ assert (e.getMessage().equals("This user's email is already associated with another user ID"));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePrimarySucceedsEvenIfAnotherAccountWithSameEmailButInADifferentTenant()
+ throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null),
+ new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true),
+ new JsonObject()));
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1",
+ StorageLayer.getStorage(process.main));
+ AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(),
+ "test@example.com",
+ "pass1234");
+
+ AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId());
+ assert (!result.wasAlreadyAPrimaryUser);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google",
+ "test@example.com");
+
+ AuthRecipe.CreatePrimaryUserResult r = AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId());
+ assert !r.wasAlreadyAPrimaryUser;
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makePrimaryUserFailsCauseOfUnknownUserId() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ try {
+ AuthRecipe.createPrimaryUser(process.main, "random");
+ assert (false);
+ } catch (UnknownUserIdException ignored) {
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccount() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com",
+ "pass1234");
+ AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com",
+ "pass1234");
+
+ AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId());
+
+ try {
+ AuthRecipe.createPrimaryUser(process.main, emailPasswordUser2.getSupertokensUserId());
+ assert (false);
+ } catch (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException e) {
+ assert (e.primaryUserId.equals(emailPasswordUser1.getSupertokensUserId()));
+ assert (e.getMessage().equals("This user ID is already linked to another user ID"));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/DeleteUserTest.java b/src/test/java/io/supertokens/test/accountlinking/DeleteUserTest.java
new file mode 100644
index 000000000..edc07511d
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/DeleteUserTest.java
@@ -0,0 +1,394 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.useridmapping.UserIdType;
+import io.supertokens.usermetadata.UserMetadata;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import static org.junit.Assert.*;
+
+
+public class DeleteUserTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void deleteLinkedUserWithoutRemovingAllUsers() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId(), false);
+
+ assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()));
+
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+
+ assert (user.loginMethods.length == 1);
+ assert (user.isPrimaryUser);
+ assert (user.getSupertokensUserId().equals(r2.getSupertokensUserId()));
+ assert (user.loginMethods[0].getSupertokensUserId().equals(r2.getSupertokensUserId()));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void deleteLinkedPrimaryUserWithoutRemovingAllUsers() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r2.getSupertokensUserId(), false);
+
+ AuthRecipeUserInfo userP = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r1.getSupertokensUserId());
+
+ assert (user.loginMethods.length == 1);
+ assert (user.isPrimaryUser);
+ assert (user.getSupertokensUserId().equals(r2.getSupertokensUserId()));
+ assert (user.loginMethods[0].getSupertokensUserId().equals(r1.getSupertokensUserId()));
+ assert (userP.equals(user));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void deleteLinkedPrimaryUserRemovingAllUsers() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r2.getSupertokensUserId());
+
+ AuthRecipeUserInfo userP = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r1.getSupertokensUserId());
+
+ assert (user == null && userP == null);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void deleteLinkedPrimaryUserRemovingAllUsers2() throws Exception {
+
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId());
+
+ AuthRecipeUserInfo userP = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r1.getSupertokensUserId());
+
+ assert (user == null && userP == null);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void deleteUserTestWithUserIdMapping1() throws Exception {
+ /*
+ * recipe user r1 is mapped to e1 which has some metadata with e1 as the key. r1 gets linked to r2 which is
+ * mapped to e2 with some metadata associated with it. Now we want to delete r1. This should clear r1 entry,
+ * e1 entry, and e1 metadata, but should not clear e2 stuff at all.
+ * */
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r1.getSupertokensUserId(), "e1", null, false);
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("k1", "v1");
+ UserMetadata.updateUserMetadata(process.main, "e1", metadata);
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r2.getSupertokensUserId(), "e2", null, false);
+ UserMetadata.updateUserMetadata(process.main, "e2", metadata);
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId(), false);
+
+ assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()));
+
+ assertNull(AuthRecipe.getUserById(process.main, "e2"));
+
+ assertNotNull(AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()));
+
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e1"), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, r1.getSupertokensUserId()), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), metadata);
+ assertEquals(UserMetadata.getUserMetadata(process.main, r2.getSupertokensUserId()), new JsonObject());
+ assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void deleteUserTestWithUserIdMapping2() throws Exception {
+ /*
+ * recipe user r1 exists. r1 gets linked to r2 which is mapped to e2 with some metadata associated with it.
+ * Now we want to delete r1 with linked all recipes as true. This should clear r1, r2 entry clear e2 metadata.
+ * */
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r1.getSupertokensUserId(), "e1", null, false);
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("k1", "v1");
+ UserMetadata.updateUserMetadata(process.main, "e1", metadata);
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r2.getSupertokensUserId(), "e2", null, false);
+ UserMetadata.updateUserMetadata(process.main, "e2", metadata);
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId());
+
+ assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()));
+
+ assertNull(AuthRecipe.getUserById(process.main, "e2"));
+
+ assertNull(AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()));
+
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e1"), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, r1.getSupertokensUserId()), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, r2.getSupertokensUserId()), new JsonObject());
+ assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void deleteUserTestWithUserIdMapping3() throws Exception {
+ /*
+ * three recipes are linked, r1, r2, r3 (primary user is r1). We have external user ID mapping for all three
+ * with some metadata. First we delete r1. This should not delete metadata and linked accounts. Then we
+ * delete r2 - this should delete metadata of r2 and r2. The we delete r3 - this should delete metadata of r3
+ * and r1
+ * */
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r1.getSupertokensUserId(), "e1", null, false);
+ JsonObject metadata = new JsonObject();
+ metadata.addProperty("k1", "v1");
+ UserMetadata.updateUserMetadata(process.main, "e1", metadata);
+
+ AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r2.getSupertokensUserId(), "e2", null, false);
+ UserMetadata.updateUserMetadata(process.main, "e2", metadata);
+
+ AuthRecipeUserInfo r3 = EmailPassword.signUp(process.main, "test3@example.com", "pass123");
+ UserIdMapping.createUserIdMapping(process.main, r3.getSupertokensUserId(), "e3", null, false);
+ UserMetadata.updateUserMetadata(process.main, "e3", metadata);
+
+ AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId());
+
+ assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked);
+ assert (!AuthRecipe.linkAccounts(process.main, r3.getSupertokensUserId(), r1.getSupertokensUserId()).wasAlreadyLinked);
+
+ AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId(), false);
+
+ assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()));
+
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e1"), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, r1.getSupertokensUserId()), new JsonObject());
+
+ {
+ AuthRecipeUserInfo userR2 = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+ AuthRecipeUserInfo userR3 = AuthRecipe.getUserById(process.main, r3.getSupertokensUserId());
+ assert (userR2.equals(userR3));
+ assert (userR2.loginMethods.length == 2);
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), metadata);
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e3"), metadata);
+ assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r3.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+ }
+
+ AuthRecipe.deleteUser(process.main, r2.getSupertokensUserId(), false);
+
+ {
+ AuthRecipeUserInfo userR2 = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+ AuthRecipeUserInfo userR3 = AuthRecipe.getUserById(process.main, r3.getSupertokensUserId());
+ assert (userR2.equals(userR3));
+ assert (userR2.loginMethods.length == 1);
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), metadata);
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e3"), metadata);
+ assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r3.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+ }
+
+ AuthRecipe.deleteUser(process.main, r3.getSupertokensUserId(), false);
+
+ {
+ AuthRecipeUserInfo userR2 = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId());
+ AuthRecipeUserInfo userR3 = AuthRecipe.getUserById(process.main, r3.getSupertokensUserId());
+ assert (userR2 == null && userR3 == null);
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), new JsonObject());
+ assertEquals(UserMetadata.getUserMetadata(process.main, "e3"), new JsonObject());
+ assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r3.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+ assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null);
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/GetUserByAccountInfoTest.java b/src/test/java/io/supertokens/test/accountlinking/GetUserByAccountInfoTest.java
new file mode 100644
index 000000000..11371bf97
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/GetUserByAccountInfoTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import io.supertokens.Main;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.passwordless.Passwordless;
+import io.supertokens.passwordless.exceptions.*;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
+import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.ThirdParty;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+
+public class GetUserByAccountInfoTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password)
+ throws DuplicateEmailException, StorageQueryException {
+ return EmailPassword.signUp(main, email, password);
+ }
+
+ AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email)
+ throws EmailChangeNotAllowedException, StorageQueryException {
+ return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user;
+ }
+
+ AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email)
+ throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException,
+ RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException,
+ StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null,
+ null, "123456");
+ return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash,
+ code.userInputCode, null).user;
+ }
+
+ AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone)
+ throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException,
+ RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException,
+ StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone,
+ null, "123456");
+ return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash,
+ code.userInputCode, null).user;
+ }
+
+ @Test
+ public void testListUsersByAccountInfoForUnlinkedAccounts() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1");
+ AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com");
+ AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com");
+ AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210");
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess()));
+
+ AuthRecipeUserInfo userToTest = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, null, null)[0];
+ assertNotNull(userToTest.getSupertokensUserId());
+ assertFalse(userToTest.isPrimaryUser);
+ assertEquals(1, userToTest.loginMethods.length);
+ assertEquals("test1@example.com", userToTest.loginMethods[0].email);
+ assertEquals(RECIPE_ID.EMAIL_PASSWORD, userToTest.loginMethods[0].recipeId);
+ assertEquals(user1.getSupertokensUserId(), userToTest.loginMethods[0].getSupertokensUserId());
+ assertFalse(userToTest.loginMethods[0].verified);
+ assert(userToTest.loginMethods[0].timeJoined > 0);
+
+ // test for result
+ assertEquals(user1, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@example.com", null, null, null)[0]);
+ assertEquals(user2, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, null, "google", "userid1")[0]);
+ assertEquals(user2, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test2@example.com", null, "google", "userid1")[0]);
+ assertEquals(user3, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@example.com", null, null, null)[0]);
+ assertEquals(user4, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, "+919876543210", null, null)[0]);
+
+ // test for no result
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@example.com", "+919876543210", null, null).length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test2@example.com", "+919876543210", null, null).length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@example.com", "+919876543210", null, null).length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, "+919876543210", "google", "userid1").length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@gmail.com", null, "google", "userid1").length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@gmail.com", null, "google", "userid1").length);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testListUsersByAccountInfoForUnlinkedAccountsWithUnionOption() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1");
+ AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com");
+ AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com");
+ AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210");
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess()));
+ {
+ AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, "test1@example.com", "+919876543210", null, null);
+ assertEquals(2, users.length);
+ assertTrue(Arrays.asList(users).contains(user1));
+ assertTrue(Arrays.asList(users).contains(user4));
+ }
+ {
+ AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, "test1@example.com", null, "google", "userid1");
+ assertEquals(2, users.length);
+ assertTrue(Arrays.asList(users).contains(user1));
+ assertTrue(Arrays.asList(users).contains(user2));
+ }
+ {
+ AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, null, "+919876543210", "google", "userid1");
+ assertEquals(2, users.length);
+ assertTrue(Arrays.asList(users).contains(user4));
+ assertTrue(Arrays.asList(users).contains(user2));
+ }
+ {
+ AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, "test1@example.com", "+919876543210", "google", "userid1");
+ assertEquals(3, users.length);
+ assertTrue(Arrays.asList(users).contains(user1));
+ assertTrue(Arrays.asList(users).contains(user2));
+ assertTrue(Arrays.asList(users).contains(user4));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testUnknownAccountInfo() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess()));
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@example.com", null, null, null).length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, null, "google", "userid1").length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@example.com", null, null, null).length);
+ assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, "+919876543210", null, null).length);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testListUserByAccountInfoWhenAccountsAreLinked1() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "userid1", "test2@example.com").user;
+
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user;
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(
+ StorageLayer.getBaseStorage(process.getProcess()));
+
+ primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId());
+
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test2@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ null, null, "google", "userid1")[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, "google", "userid1")[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test2@example.com", null, "google", "userid1")[0]);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testListUserByAccountInfoWhenAccountsAreLinked2() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password2");
+
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user;
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(
+ StorageLayer.getBaseStorage(process.getProcess()));
+
+ primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId());
+
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test2@example.com", null, null, null)[0]);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testListUserByAccountInfoWhenAccountsAreLinked3() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test2@example.com");
+
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user;
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(
+ StorageLayer.getBaseStorage(process.getProcess()));
+
+ primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId());
+
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test2@example.com", null, null, null)[0]);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testListUserByAccountInfoWhenAccountsAreLinked4() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210");
+
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user;
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(
+ StorageLayer.getBaseStorage(process.getProcess()));
+
+ primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId());
+
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ null, "+919876543210", null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", "+919876543210", null, null)[0]);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testListUserByAccountInfoWhenAccountsAreLinked5() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com");
+
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user;
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(
+ StorageLayer.getBaseStorage(process.getProcess()));
+
+ primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId());
+
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test2@example.com", null, null, null)[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ null, null, "google", "userid1")[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test1@example.com", null, "google", "userid1")[0]);
+ assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false,
+ "test2@example.com", null, "google", "userid1")[0]);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/GetUserByIdTest.java b/src/test/java/io/supertokens/test/accountlinking/GetUserByIdTest.java
new file mode 100644
index 000000000..48bf9ee9a
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/GetUserByIdTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import io.supertokens.Main;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.passwordless.Passwordless;
+import io.supertokens.passwordless.exceptions.*;
+import io.supertokens.pluginInterface.RECIPE_ID;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
+import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.ThirdParty;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.*;
+
+public class GetUserByIdTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password)
+ throws DuplicateEmailException, StorageQueryException {
+ return EmailPassword.signUp(main, email, password);
+ }
+
+ AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email)
+ throws EmailChangeNotAllowedException, StorageQueryException {
+ return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user;
+ }
+
+ AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email)
+ throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException,
+ RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException,
+ StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null,
+ null, "123456");
+ return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash,
+ code.userInputCode, null).user;
+ }
+
+ AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone)
+ throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException,
+ RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException,
+ StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone,
+ null, "123456");
+ return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash,
+ code.userInputCode, null).user;
+ }
+
+ @Test
+ public void testAllLoginMethods() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210");
+
+ assertFalse(user1.isPrimaryUser);
+ assertFalse(user2.isPrimaryUser);
+ assertFalse(user3.isPrimaryUser);
+ assertFalse(user4.isPrimaryUser);
+
+ AuthRecipeUserInfo userToTest = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId());
+ assertNotNull(userToTest.getSupertokensUserId());
+ assertFalse(userToTest.isPrimaryUser);
+ assertEquals(1, userToTest.loginMethods.length);
+ assertEquals("test@example.com", userToTest.loginMethods[0].email);
+ assertEquals(RECIPE_ID.EMAIL_PASSWORD, userToTest.loginMethods[0].recipeId);
+ assertEquals(user1.getSupertokensUserId(), userToTest.loginMethods[0].getSupertokensUserId());
+ assertFalse(userToTest.loginMethods[0].verified);
+ assert(userToTest.loginMethods[0].timeJoined > 0);
+
+ assertEquals(user1, AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()));
+ assertEquals(user2, AuthRecipe.getUserById(process.getProcess(), user2.getSupertokensUserId()));
+ assertEquals(user3, AuthRecipe.getUserById(process.getProcess(), user3.getSupertokensUserId()));
+ assertEquals(user4, AuthRecipe.getUserById(process.getProcess(), user4.getSupertokensUserId()));
+
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user4.getSupertokensUserId()).user;
+ AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), user1.getSupertokensUserId(), primaryUser.getSupertokensUserId());
+
+ for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId(), user4.getSupertokensUserId()}) {
+ AuthRecipeUserInfo result = AuthRecipe.getUserById(process.getProcess(), userId);
+ assertTrue(result.isPrimaryUser);
+
+ assertEquals(4, result.loginMethods.length);
+ assertEquals(user1.loginMethods[0], result.loginMethods[0]);
+ assertEquals(user2.loginMethods[0], result.loginMethods[1]);
+ assertEquals(user3.loginMethods[0], result.loginMethods[2]);
+ assertEquals(user4.loginMethods[0], result.loginMethods[3]);
+
+ assertEquals(user1.timeJoined, result.timeJoined);
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testUnknownUserIdReturnsNull() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ assertNull(AuthRecipe.getUserById(process.getProcess(), "unknownid"));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testLoginMethodsAreSortedByTime() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ // Create users
+ AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com");
+ Thread.sleep(50);
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1");
+
+ // Link accounts randomly
+ String[] userIds = new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId(), user4.getSupertokensUserId()};
+ Collections.shuffle(Arrays.asList(userIds));
+ AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), userIds[0]).user;
+ AuthRecipe.linkAccounts(process.getProcess(), userIds[1], primaryUser.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), userIds[2], primaryUser.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), userIds[3], primaryUser.getSupertokensUserId());
+
+ for (String userId : userIds) {
+ AuthRecipeUserInfo result = AuthRecipe.getUserById(process.getProcess(), userId);
+ assertTrue(result.isPrimaryUser);
+
+ assertEquals(4, result.loginMethods.length);
+ assert(result.loginMethods[0].timeJoined <= result.loginMethods[1].timeJoined);
+ assert(result.loginMethods[1].timeJoined <= result.loginMethods[2].timeJoined);
+ assert(result.loginMethods[2].timeJoined <= result.loginMethods[3].timeJoined);
+ }
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java
new file mode 100644
index 000000000..aab79a23a
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java
@@ -0,0 +1,607 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
+import io.supertokens.multitenancy.Multitenancy;
+import io.supertokens.passwordless.Passwordless;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.multitenancy.*;
+import io.supertokens.session.Session;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.ThirdParty;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import static org.junit.Assert.assertNotNull;
+
+
+public class LinkAccountsTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void linkAccountSuccess() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password");
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject());
+ String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (refetchUser2.equals(refetchUser));
+ assert (refetchUser2.loginMethods.length == 2);
+ assert (refetchUser.loginMethods[0].equals(user.loginMethods[0]));
+ assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0]));
+ assert (refetchUser.tenantIds.size() == 1);
+ assert (refetchUser.isPrimaryUser);
+ assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId()));
+
+ // cause linkAccounts revokes sessions for the recipe user ID
+ sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 0);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountSuccessWithSameEmail() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject());
+ String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (refetchUser2.equals(refetchUser));
+ assert (refetchUser2.loginMethods.length == 2);
+ assert (refetchUser.loginMethods[0].equals(user.loginMethods[0]));
+ assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0]));
+ assert (refetchUser.tenantIds.size() == 1);
+ assert (refetchUser.isPrimaryUser);
+ assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId()));
+
+ // cause linkAccounts revokes sessions for the recipe user ID
+ sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 0);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testThatLinkingAccountsRequiresAccountLinkingFeatureToBeEnabled() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.isInMemDb(process.getProcess())) {
+ return;
+ }
+
+ try {
+ AuthRecipe.linkAccounts(process.main, "", "");
+ assert (false);
+ } catch (FeatureNotEnabledException e) {
+ assert (e.getMessage()
+ .equals("Account linking feature is not enabled for this app. Please contact support to enable it" +
+ "."));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountSuccessEvenIfUsingRecipeUserIdThatIsLinkedToPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password");
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password");
+ assert (!user3.isPrimaryUser);
+
+ wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user3.getSupertokensUserId(), user2.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (refetchUser.loginMethods.length == 3);
+ assert (refetchUser.loginMethods[0].equals(user.loginMethods[0]));
+ assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0]));
+ assert (refetchUser.loginMethods[2].equals(user3.loginMethods[0]));
+ assert (refetchUser.tenantIds.size() == 1);
+ assert (refetchUser.isPrimaryUser);
+ assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId()));
+
+ AuthRecipeUserInfo refetchUser3 = AuthRecipe.getUserById(process.main, user3.getSupertokensUserId());
+ assert (refetchUser3.equals(refetchUser));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void alreadyLinkAccountLinkAgain() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject());
+ String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (wasAlreadyLinked);
+
+ // cause linkAccounts revokes sessions for the recipe user ID
+ sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountFailureCauseRecipeUserIdLinkedWithAnotherPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password");
+ assert (!user.isPrimaryUser);
+ AuthRecipe.createPrimaryUser(process.main, user3.getSupertokensUserId());
+
+ try {
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user3.getSupertokensUserId());
+ assert (false);
+ } catch (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException e) {
+ assert (e.recipeUser.getSupertokensUserId().equals(user.getSupertokensUserId()));
+ assert (e.getMessage().equals("The input recipe user ID is already linked to another user ID"));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountFailureInputUserIsNotAPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ try {
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId());
+ assert (false);
+ } catch (InputUserIdIsNotAPrimaryUserException e) {
+ assert (e.userId.equals(user.getSupertokensUserId()));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountFailureUserDoesNotExist() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ try {
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), "random");
+ assert (false);
+ } catch (UnknownUserIdException e) {
+ }
+
+ try {
+ AuthRecipe.linkAccounts(process.main, "random2", user.getSupertokensUserId());
+ assert (false);
+ } catch (UnknownUserIdException e) {
+ }
+
+ try {
+ AuthRecipe.linkAccounts(process.main, "random2", "random");
+ assert (false);
+ } catch (UnknownUserIdException e) {
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Thread.sleep(50);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipeUserInfo otherPrimaryUser = EmailPassword.signUp(process.getProcess(), "test3@example.com",
+ "password");
+
+ AuthRecipe.createPrimaryUser(process.main, otherPrimaryUser.getSupertokensUserId());
+
+ try {
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), otherPrimaryUser.getSupertokensUserId());
+ assert (false);
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ assert (e.primaryUserId.equals(user.getSupertokensUserId()));
+ assert (e.getMessage().equals("This user's email is already associated with another user ID"));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUserEvenIfInDifferentTenant() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null),
+ new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true),
+ new JsonObject()));
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1",
+ StorageLayer.getStorage(process.main));
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(),
+ "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Thread.sleep(50);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(tenantIdentifierWithStorage,
+ process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipeUserInfo otherPrimaryUser = EmailPassword.signUp(process.getProcess(), "test3@example.com",
+ "password");
+
+ AuthRecipe.createPrimaryUser(process.main, otherPrimaryUser.getSupertokensUserId());
+
+ try {
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), otherPrimaryUser.getSupertokensUserId());
+ assert (false);
+ } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
+ assert (e.primaryUserId.equals(user.getSupertokensUserId()));
+ assert (e.getMessage().equals("This user's email is already associated with another user ID"));
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountSuccessAcrossTenants() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null),
+ new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true),
+ new JsonObject()));
+
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1",
+ StorageLayer.getStorage(process.main));
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(),
+ "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Thread.sleep(50);
+
+ ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(
+ process.getProcess(), "google",
+ "user-google",
+ "test@example.com");
+ AuthRecipeUserInfo user2 = signInUpResponse.user;
+ assert (!user2.isPrimaryUser);
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo refetchedUser1 = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ AuthRecipeUserInfo refetchedUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+ assert (refetchedUser1.getSupertokensUserId().equals(refetchedUser2.getSupertokensUserId()));
+ assert refetchedUser1.loginMethods.length == 2;
+ assert refetchedUser1.tenantIds.size() == 2;
+ assert refetchedUser1.tenantIds.contains("t1");
+ assert refetchedUser1.tenantIds.contains("public");
+ assert refetchedUser1.getSupertokensUserId().equals(user.getSupertokensUserId());
+ assert refetchedUser1.isPrimaryUser;
+ assert refetchedUser1.loginMethods[0].getSupertokensUserId().equals(user.loginMethods[0].getSupertokensUserId());
+ assert refetchedUser1.loginMethods[1].getSupertokensUserId().equals(user2.loginMethods[0].getSupertokensUserId());
+
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void linkAccountSuccessWithPasswordlessEmailAndPhoneNumber() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), "u@e.com", null, null,
+ null);
+ Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId,
+ code.deviceIdHash, code.userInputCode, null);
+ AuthRecipeUserInfo user2 = pResp.user;
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Passwordless.updateUser(process.main, user2.getSupertokensUserId(), null, new Passwordless.FieldUpdate("1234"));
+ user2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+
+ boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked;
+ assert (!wasAlreadyLinked);
+
+ AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (refetchUser2.equals(refetchUser));
+ assert (refetchUser2.loginMethods.length == 2);
+ assert (refetchUser.loginMethods[0].equals(user.loginMethods[0]));
+ assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0]));
+ assert (refetchUser.tenantIds.size() == 1);
+ assert (refetchUser.isPrimaryUser);
+ assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId()));
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java
new file mode 100644
index 000000000..e9f34f4d6
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java
@@ -0,0 +1,1051 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.Main;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
+import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
+import io.supertokens.emailpassword.exceptions.WrongCredentialsException;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
+import io.supertokens.multitenancy.Multitenancy;
+import io.supertokens.multitenancy.exception.*;
+import io.supertokens.passwordless.Passwordless;
+import io.supertokens.passwordless.exceptions.PhoneNumberChangeNotAllowedException;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.authRecipe.LoginMethod;
+import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.multitenancy.*;
+import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
+import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException;
+import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.InvalidProviderConfigException;
+import io.supertokens.thirdparty.ThirdParty;
+import io.supertokens.userroles.UserRoles;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class MultitenantTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ TenantIdentifier t1, t2, t3, t4;
+
+ private void createTenants(Main main)
+ throws StorageQueryException, TenantOrAppNotFoundException, InvalidProviderConfigException,
+ FeatureNotEnabledException, IOException, InvalidConfigException,
+ CannotModifyBaseConfigException, BadPermissionException {
+ // User pool 1 - (null, a1, null)
+ // User pool 2 - (null, a1, t1), (null, a1, t2)
+
+ { // tenant 1
+ JsonObject config = new JsonObject();
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null);
+
+ StorageLayer.getStorage(new TenantIdentifier(null, null, null), main)
+ .modifyConfigToAddANewUserPoolForTesting(config, 1);
+
+ Multitenancy.addNewOrUpdateAppOrTenant(
+ main,
+ new TenantIdentifier(null, null, null),
+ new TenantConfig(
+ tenantIdentifier,
+ new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, null),
+ new PasswordlessConfig(true),
+ config
+ )
+ );
+ }
+
+ { // tenant 2
+ JsonObject config = new JsonObject();
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t1");
+
+ StorageLayer.getStorage(new TenantIdentifier(null, null, null), main)
+ .modifyConfigToAddANewUserPoolForTesting(config, 1);
+
+ Multitenancy.addNewOrUpdateAppOrTenant(
+ main,
+ new TenantIdentifier(null, "a1", null),
+ new TenantConfig(
+ tenantIdentifier,
+ new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, null),
+ new PasswordlessConfig(true),
+ config
+ )
+ );
+ }
+
+ { // tenant 3
+ JsonObject config = new JsonObject();
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t2");
+
+ StorageLayer.getStorage(new TenantIdentifier(null, null, null), main)
+ .modifyConfigToAddANewUserPoolForTesting(config, 1);
+
+ Multitenancy.addNewOrUpdateAppOrTenant(
+ main,
+ new TenantIdentifier(null, "a1", null),
+ new TenantConfig(
+ tenantIdentifier,
+ new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, null),
+ new PasswordlessConfig(true),
+ config
+ )
+ );
+ }
+
+ { // tenant 4
+ JsonObject config = new JsonObject();
+ TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t3");
+
+ StorageLayer.getStorage(new TenantIdentifier(null, null, null), main)
+ .modifyConfigToAddANewUserPoolForTesting(config, 1);
+
+ Multitenancy.addNewOrUpdateAppOrTenant(
+ main,
+ new TenantIdentifier(null, "a1", null),
+ new TenantConfig(
+ tenantIdentifier,
+ new EmailPasswordConfig(true),
+ new ThirdPartyConfig(true, null),
+ new PasswordlessConfig(true),
+ config
+ )
+ );
+ }
+ }
+
+ @Test
+ public void testUserAreNotAutomaticallySharedBetweenTenantsOfLinkedAccountsForPless() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ createTenants(process.getProcess());
+
+ t1 = new TenantIdentifier(null, "a1", null);
+ t2 = new TenantIdentifier(null, "a1", "t1");
+
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess()));
+ TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess()));
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test@example.com", "password");
+ Passwordless.CreateCodeResponse user2Code = Passwordless.createCode(t1WithStorage, process.getProcess(),
+ "test@example.com", null, null, null);
+ AuthRecipeUserInfo user2 = Passwordless.consumeCode(t1WithStorage, process.getProcess(), user2Code.deviceId, user2Code.deviceIdHash, user2Code.userInputCode, null).user;
+
+ AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId());
+
+ { // user2 should not be shared in tenant2
+ Passwordless.CreateCodeResponse user3Code = Passwordless.createCode(t2WithStorage, process.getProcess(),
+ "test@example.com", null, null, null);
+ Passwordless.ConsumeCodeResponse res = Passwordless.consumeCode(t2WithStorage, process.getProcess(),
+ user3Code.deviceId, user3Code.deviceIdHash, user3Code.userInputCode, null);
+ assertTrue(res.createdNewUser);
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testUserAreNotAutomaticallySharedBetweenTenantsOfLinkedAccountsForTP() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ createTenants(process.getProcess());
+
+ t1 = new TenantIdentifier(null, "a1", null);
+ t2 = new TenantIdentifier(null, "a1", "t1");
+
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess()));
+ TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess()));
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test@example.com", "password");
+ AuthRecipeUserInfo user2 = ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid1", "test@example.com").user;
+
+ AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId());
+
+ { // user2 should not be shared in tenant2
+ ThirdParty.SignInUpResponse res = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google",
+ "googleid1", "test@example.com");
+ assertTrue(res.createdNewUser);
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testTenantDeletionWithAccountLinking() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ createTenants(process.getProcess());
+
+ t1 = new TenantIdentifier(null, "a1", null);
+ t2 = new TenantIdentifier(null, "a1", "t1");
+
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess()));
+ TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess()));
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test@example.com", "password");
+ AuthRecipeUserInfo user2 = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", "googleid1", "test@example.com").user;
+
+ AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ Multitenancy.deleteTenant(t2, process.getProcess());
+
+ AuthRecipeUserInfo getUser1 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId());
+ for (LoginMethod lm : getUser1.loginMethods) {
+ assertEquals(0, lm.tenantIds.size());
+ }
+
+ AuthRecipeUserInfo getUser2 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId());
+ for (LoginMethod lm : getUser2.loginMethods) {
+ assertEquals(0, lm.tenantIds.size());
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testTenantDeletionWithAccountLinkingWithUserRoles() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ createTenants(process.getProcess());
+
+ t1 = new TenantIdentifier(null, "a1", null);
+ t2 = new TenantIdentifier(null, "a1", "t1");
+
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess()));
+ TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess()));
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test@example.com", "password");
+ AuthRecipeUserInfo user2 = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", "googleid1", "test@example.com").user;
+
+ AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ UserRoles.createNewRoleOrModifyItsPermissions(t2WithStorage.toAppIdentifierWithStorage(), "admin", new String[]{"p1"});
+ UserRoles.addRoleToUser(t2WithStorage, user1.getSupertokensUserId(), "admin");
+
+ Multitenancy.deleteTenant(t2, process.getProcess());
+
+ createTenants(process.getProcess()); // create the tenant again
+
+ Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId()); // add the user to the tenant again
+ Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user2.getSupertokensUserId()); // add the user to the tenant again
+
+ AuthRecipeUserInfo getUser1 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId());
+ for (LoginMethod lm : getUser1.loginMethods) {
+ assertEquals(1, lm.tenantIds.size());
+ }
+
+ AuthRecipeUserInfo getUser2 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId());
+ for (LoginMethod lm : getUser2.loginMethods) {
+ assertEquals(1, lm.tenantIds.size());
+ }
+
+ String[] roles = UserRoles.getRolesForUser(t2WithStorage, user1.getSupertokensUserId());
+ assertEquals(0, roles.length); // must be deleted with tenant
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testVariousCases() throws Exception {
+ t1 = new TenantIdentifier(null, "a1", null);
+ t2 = new TenantIdentifier(null, "a1", "t1");
+ t3 = new TenantIdentifier(null, "a1", "t2");
+ t4 = new TenantIdentifier(null, "a1", "t3");
+
+ TestCase[] testCases = new TestCase[]{
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new CreatePlessUserWithEmail(t2, "test@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new AssociateUserToTenant(t2, 0),
+ }),
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new CreatePlessUserWithEmail(t2, "test@example.com"),
+ new AssociateUserToTenant(t2, 0),
+ new MakePrimaryUser(t1, 0),
+ }),
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new CreatePlessUserWithEmail(t2, "test@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new SignInEmailPasswordUser(t2, 0).expect(new WrongCredentialsException())
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()),
+ new AssociateUserToTenant(t2, 2), // Allowed
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t2, 2),
+ new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreatePlessUserWithEmail(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()),
+ new AssociateUserToTenant(t2, 2), // Allowed
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreatePlessUserWithEmail(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreatePlessUserWithEmail(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t2, 2),
+ new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t1, 2),
+ new AssociateUserToTenant(t2, 2), // Allowed
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t2, 2),
+ new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test1@example.com"),
+ new CreatePlessUserWithEmail(t2, "test2@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new UpdatePlessUserEmail(t1, 0, "test2@example.com"),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test1@example.com"),
+ new CreatePlessUserWithEmail(t1, "test3@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new UpdatePlessUserEmail(t1, 0, "test3@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test1@example.com"),
+ new CreatePlessUserWithEmail(t1, "test3@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new UpdatePlessUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithPhone(t1, "+1000001"),
+ new CreatePlessUserWithPhone(t1, "+1000003"),
+ new CreatePlessUserWithPhone(t2, "+1000002"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new UpdatePlessUserPhone(t1, 0, "+1000003").expect(new PhoneNumberChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithPhone(t1, "+1000001"),
+ new CreatePlessUserWithPhone(t1, "+1000003"),
+ new CreatePlessUserWithPhone(t2, "+1000002"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new UpdatePlessUserPhone(t1, 1, "+1000001").expect(new PhoneNumberChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t1, "test3@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new UpdateEmailPasswordUserEmail(t1, 0, "test3@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t1, "test3@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new UpdateEmailPasswordUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()),
+ new AssociateUserToTenant(t2, 2), // Allowed
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t2, 2),
+ new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t1, 2),
+ new AssociateUserToTenant(t2, 2), // Allowed
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new CreateEmailPasswordUser(t3, "test1@example.com"),
+ new AssociateUserToTenant(t2, 2),
+ new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"),
+ new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test2@example.com"),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"),
+ new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test3@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"),
+ new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"),
+ new CreateEmailPasswordUser(t2, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 2),
+ new MakePrimaryUser(t2, 1),
+ new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithPhone(t1, "+1000001"),
+ new CreatePlessUserWithPhone(t2, "+1000002"),
+ new CreatePlessUserWithPhone(t3, "+1000001"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t1, 2).expect(new DuplicatePhoneNumberException()),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException("")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"),
+ new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"),
+ new CreateThirdPartyUser(t3, "google", "googleid1", "test3@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new MakePrimaryUser(t3, 2),
+ new AssociateUserToTenant(t1, 2).expect(new DuplicateThirdPartyUserException()),
+ new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException("")),
+ }),
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"),
+ new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"),
+ new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new MakePrimaryUser(t1, 2),
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test3@example.com").expect(new EmailChangeNotAllowedException()),
+ new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()),
+ }),
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new CreateEmailPasswordUser(t1, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new UnlinkAccount(t1, 0),
+ new AssociateUserToTenant(t2, 0).expect(new UnknownUserIdException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreatePlessUserWithEmail(t1, "test@example.com"),
+ new CreatePlessUserWithEmail(t1, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new UnlinkAccount(t1, 0),
+ new AssociateUserToTenant(t2, 0).expect(new UnknownUserIdException()),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateThirdPartyUser(t1, "google", "googleid1", "test@example.com"),
+ new CreateThirdPartyUser(t1, "google", "googleid2", "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new UnlinkAccount(t1, 0),
+ new AssociateUserToTenant(t2, 0).expect(new UnknownUserIdException()),
+ }),
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new CreateEmailPasswordUser(t1, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new DisassociateUserFromTenant(t1, 0),
+ new AssociateUserToTenant(t2, 0),
+ new TestCaseStep() {
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, main));
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), TestCase.users.get(0).getSupertokensUserId());
+ assertEquals(2, user.loginMethods.length);
+ assertTrue(user.loginMethods[0].tenantIds.contains(t2.getTenantId()));
+ assertTrue(user.loginMethods[1].tenantIds.contains(t1.getTenantId()));
+ }
+ }
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new DisassociateUserFromTenant(t1, 0),
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new DisassociateUserFromTenant(t1, 1),
+ new MakePrimaryUser(t1, 0),
+ new MakePrimaryUser(t1, 1),
+ new AssociateUserToTenant(t1, 0),
+ new AssociateUserToTenant(t1, 1).expect(new DuplicateEmailException()),
+ new LinkAccounts(t1, 0, 1).expect(new RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(null, "")),
+ }),
+
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new DisassociateUserFromTenant(t1, 0),
+ new CreateEmailPasswordUser(t1, "test@example.com"),
+ new DisassociateUserFromTenant(t1, 1),
+ new MakePrimaryUser(t1, 0),
+ new AssociateUserToTenant(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new AssociateUserToTenant(t1, 1).expect(new DuplicateEmailException()),
+ new AssociateUserToTenant(t2, 1),
+ }),
+ new TestCase(new TestCaseStep[]{
+ new CreateEmailPasswordUser(t1, "test1@example.com"),
+ new CreateEmailPasswordUser(t1, "test2@example.com"),
+ new MakePrimaryUser(t1, 0),
+ new LinkAccounts(t1, 0, 1),
+ new UnlinkAccount(t1, 0),
+ new TestCaseStep() {
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, main));
+ AuthRecipe.deleteUser(t1WithStorage.toAppIdentifierWithStorage(), TestCase.users.get(1).getSupertokensUserId());
+ }
+ },
+ new TestCaseStep() {
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, main));
+ AuthRecipeUserInfo user = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), TestCase.users.get(0).getSupertokensUserId());
+ assertNull(user);
+ }
+ }
+ }),
+ };
+
+ int i = 0;
+ for (TestCase testCase : testCases) {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ createTenants(process.getProcess());
+
+ System.out.println("Executing test case : " + i);
+ testCase.doTest(process.getProcess());
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ i++;
+ }
+ }
+
+ private static class TestCase {
+ TestCaseStep[] steps;
+ public static List users;
+
+ public static void resetUsers() {
+ users = new ArrayList<>();
+ }
+
+ public static void addUser(AuthRecipeUserInfo user) {
+ users.add(user);
+ }
+
+ public TestCase(TestCaseStep[] steps) {
+ this.steps = steps;
+ }
+
+ public void doTest(Main main) throws Exception {
+ TestCase.resetUsers();
+
+ for (TestCaseStep step : steps) {
+ step.doStep(main);
+ }
+ }
+ }
+
+ private static abstract class TestCaseStep {
+ Exception e;
+
+ public TestCaseStep expect(Exception e) {
+ this.e = e;
+ return this;
+ }
+
+ public void doStep(Main main) throws Exception {
+ if (e == null) {
+ this.execute(main);
+ } else {
+ try {
+ this.execute(main);
+ fail();
+ } catch (Exception e) {
+ assertEquals(this.e.getClass(), e.getClass());
+ }
+ }
+ }
+
+ abstract public void execute(Main main) throws Exception;
+ }
+
+ private static class CreateEmailPasswordUser extends TestCaseStep {
+ private final TenantIdentifier tenantIdentifier;
+ private final String email;
+
+ public CreateEmailPasswordUser(TenantIdentifier tenantIdentifier, String email) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.email = email;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, main, email, "password");
+ TestCase.addUser(user);
+ }
+ }
+
+ private static class CreatePlessUserWithEmail extends TestCaseStep {
+ private final TenantIdentifier tenantIdentifier;
+ private final String email;
+
+ public CreatePlessUserWithEmail(TenantIdentifier tenantIdentifier, String email) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.email = email;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(tenantIdentifierWithStorage, main,
+ email, null, null, null);
+ AuthRecipeUserInfo user = Passwordless.consumeCode(tenantIdentifierWithStorage, main, code.deviceId, code.deviceIdHash, code.userInputCode, null).user;
+ TestCase.addUser(user);
+ }
+ }
+
+ private static class CreatePlessUserWithPhone extends TestCaseStep {
+ private final TenantIdentifier tenantIdentifier;
+ private final String phoneNumber;
+
+ public CreatePlessUserWithPhone(TenantIdentifier tenantIdentifier, String phoneNumber) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.phoneNumber = phoneNumber;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(tenantIdentifierWithStorage, main,
+ null, phoneNumber, null, null);
+ AuthRecipeUserInfo user = Passwordless.consumeCode(tenantIdentifierWithStorage, main, code.deviceId, code.deviceIdHash, code.userInputCode, null).user;
+ TestCase.addUser(user);
+ }
+ }
+
+ private static class CreateThirdPartyUser extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ String thirdPartyId;
+ String thirdPartyUserId;
+ String email;
+
+ public CreateThirdPartyUser(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.thirdPartyId = thirdPartyId;
+ this.thirdPartyUserId = thirdPartyUserId;
+ this.email = email;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ AuthRecipeUserInfo user = ThirdParty.signInUp(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email).user;
+ TestCase.addUser(user);
+ }
+ }
+
+ private static class MakePrimaryUser extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+
+ public MakePrimaryUser(TenantIdentifier tenantIdentifier, int userIndex) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ AuthRecipe.createPrimaryUser(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId());
+ }
+ }
+
+ private static class LinkAccounts extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int primaryUserIndex;
+ int recipeUserIndex;
+
+ public LinkAccounts(TenantIdentifier tenantIdentifier, int primaryUserIndex, int recipeUserIndex) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.primaryUserIndex = primaryUserIndex;
+ this.recipeUserIndex = recipeUserIndex;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ AuthRecipe.linkAccounts(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(recipeUserIndex).getSupertokensUserId(), TestCase.users.get(primaryUserIndex).getSupertokensUserId());
+ }
+ }
+
+ private static class AssociateUserToTenant extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+
+ public AssociateUserToTenant(TenantIdentifier tenantIdentifier, int userIndex) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ Multitenancy.addUserIdToTenant(main, tenantIdentifierWithStorage, TestCase.users.get(userIndex).getSupertokensUserId());
+ }
+ }
+
+ private static class UpdateEmailPasswordUserEmail extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+ String email;
+
+ public UpdateEmailPasswordUserEmail(TenantIdentifier tenantIdentifier, int userIndex, String email) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ this.email = email;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ EmailPassword.updateUsersEmailOrPassword(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, TestCase.users.get(userIndex).getSupertokensUserId(), email, null);
+ }
+ }
+
+ private static class UpdatePlessUserEmail extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+ String email;
+
+ public UpdatePlessUserEmail(TenantIdentifier tenantIdentifier, int userIndex, String email) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ this.email = email;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), new Passwordless.FieldUpdate(email), null);
+ }
+ }
+
+ private static class UpdatePlessUserPhone extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+ String phoneNumber;
+
+ public UpdatePlessUserPhone(TenantIdentifier tenantIdentifier, int userIndex, String phoneNumber) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ this.phoneNumber = phoneNumber;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), null, new Passwordless.FieldUpdate(phoneNumber));
+ }
+ }
+
+ private static class UnlinkAccount extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+
+ public UnlinkAccount(TenantIdentifier tenantIdentifier, int userIndex) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ AuthRecipe.unlinkAccounts(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId());
+ }
+ }
+
+ private static class SignInEmailPasswordUser extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+
+ public SignInEmailPasswordUser(TenantIdentifier tenantIdentifier, int userIndex) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ EmailPassword.signIn(tenantIdentifierWithStorage, main, TestCase.users.get(userIndex).loginMethods[0].email, "password");
+ }
+ }
+
+ private static class DisassociateUserFromTenant extends TestCaseStep {
+ TenantIdentifier tenantIdentifier;
+ int userIndex;
+
+ public DisassociateUserFromTenant(TenantIdentifier tenantIdentifier, int userIndex) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.userIndex = userIndex;
+ }
+
+ @Override
+ public void execute(Main main) throws Exception {
+ TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main));
+ Multitenancy.removeUserIdFromTenant(main, tenantIdentifierWithStorage, TestCase.users.get(userIndex).getSupertokensUserId(), null);
+ }
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java
new file mode 100644
index 000000000..5a9d7cb74
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java
@@ -0,0 +1,288 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.session.Session;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import static org.junit.Assert.*;
+
+
+public class UnlinkAccountsTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void unlinkAccountSuccess() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password");
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId());
+
+ Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject());
+ String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ boolean didDelete = AuthRecipe.unlinkAccounts(process.main, user2.getSupertokensUserId());
+ assert (!didDelete);
+
+ AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+ assert (!refetchUser2.isPrimaryUser);
+ assert (refetchUser2.getSupertokensUserId().equals(user2.getSupertokensUserId()));
+ assert (refetchUser2.loginMethods.length == 1);
+ assert (refetchUser2.loginMethods[0].getSupertokensUserId().equals(user2.getSupertokensUserId()));
+
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (!refetchUser2.equals(refetchUser));
+ assert (refetchUser.isPrimaryUser);
+ assert (refetchUser.loginMethods.length == 1);
+ assert (refetchUser.loginMethods[0].getSupertokensUserId().equals(user.getSupertokensUserId()));
+
+ // cause linkAccounts revokes sessions for the recipe user ID
+ sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId());
+ assert (sessions.length == 0);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void unlinkAccountWithoutPrimaryUserId() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ try {
+ AuthRecipe.unlinkAccounts(process.main, user.getSupertokensUserId());
+ assert (false);
+ } catch (InputUserIdIsNotAPrimaryUserException e) {
+ assert (e.userId.equals(user.getSupertokensUserId()));
+ }
+
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void unlinkAccountWithUnknownUserId() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ try {
+ AuthRecipe.unlinkAccounts(process.main, "random");
+ assert (false);
+ } catch (UnknownUserIdException e) {
+ }
+
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void unlinkAccountWithPrimaryUserBecomesRecipeUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ Session.createNewSession(process.main, user.getSupertokensUserId(), new JsonObject(), new JsonObject());
+ String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ boolean didDelete = AuthRecipe.unlinkAccounts(process.main, user.getSupertokensUserId());
+ assert (!didDelete);
+
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (!refetchUser.isPrimaryUser);
+ assert (refetchUser.loginMethods.length == 1);
+ assert (refetchUser.loginMethods[0].getSupertokensUserId().equals(user.getSupertokensUserId()));
+
+ // cause linkAccounts revokes sessions for the recipe user ID
+ sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId());
+ assert (sessions.length == 0);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void unlinkAccountSuccessButDeletesUser() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password");
+ assert (!user.isPrimaryUser);
+
+ Thread.sleep(50);
+
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password");
+ assert (!user2.isPrimaryUser);
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId());
+
+ Session.createNewSession(process.main, user.getSupertokensUserId(), new JsonObject(), new JsonObject());
+ String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId());
+ assert (sessions.length == 1);
+
+ boolean didDelete = AuthRecipe.unlinkAccounts(process.main, user.getSupertokensUserId());
+ assert (didDelete);
+
+ AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId());
+ assert (refetchUser2.isPrimaryUser);
+ assert (refetchUser2.getSupertokensUserId().equals(user.getSupertokensUserId()));
+ assert (refetchUser2.loginMethods.length == 1);
+ assert (refetchUser2.loginMethods[0].getSupertokensUserId().equals(user2.getSupertokensUserId()));
+
+ AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId());
+ assert (refetchUser2.equals(refetchUser));
+
+ // cause linkAccounts revokes sessions for the recipe user ID
+ sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId());
+ assert (sessions.length == 0);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void testUnlinkUserDeletesRecipeUserAndAnotherUserLinkToIt() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password");
+ AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password");
+ AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password");
+
+ AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ AuthRecipe.unlinkAccounts(process.getProcess(), user1.getSupertokensUserId());
+
+ AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.getProcess(), user2.getSupertokensUserId());
+ assertEquals(refetchUser2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), user2.getSupertokensUserId());
+ AuthRecipeUserInfo refetchUser3 = AuthRecipe.getUserById(process.getProcess(), user3.getSupertokensUserId());
+ assertEquals(refetchUser3.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ assertEquals(refetchUser3.loginMethods.length, 2);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/UpdateUserTest.java b/src/test/java/io/supertokens/test/accountlinking/UpdateUserTest.java
new file mode 100644
index 000000000..899ca6a6a
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/UpdateUserTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 io.supertokens.test.accountlinking;
+
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.thirdparty.ThirdParty;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import static org.junit.Assert.*;
+
+public class UpdateUserTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void testThatUpdateEmailFailsWhenPassedWithNonEmailPasswordRecipeUserId() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password");
+ AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid1", "test2@example.com").user;
+
+ AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId());
+ AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId());
+
+ try {
+ EmailPassword.updateUsersEmailOrPassword(process.getProcess(), user2.getSupertokensUserId(), "test3@example.com", null);
+ fail();
+ } catch (UnknownUserIdException e) {
+ // ignore
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java
new file mode 100644
index 000000000..10c652c3c
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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 io.supertokens.test.accountlinking.api;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ActiveUsers;
+import io.supertokens.Main;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.passwordless.Passwordless;
+import io.supertokens.passwordless.exceptions.*;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException;
+import io.supertokens.pluginInterface.exceptions.StorageQueryException;
+import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
+import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.test.httpRequest.HttpRequestForTesting;
+import io.supertokens.thirdparty.ThirdParty;
+import io.supertokens.utils.SemVer;
+import io.supertokens.webserver.WebserverAPI;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ActiveUserTest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+
+ AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password)
+ throws DuplicateEmailException, StorageQueryException {
+ return EmailPassword.signUp(main, email, password);
+ }
+
+ AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email)
+ throws EmailChangeNotAllowedException, StorageQueryException {
+ return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user;
+ }
+
+ AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email)
+ throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException,
+ RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException,
+ StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null,
+ null, "123456");
+ return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash,
+ code.userInputCode, null).user;
+ }
+
+ AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone)
+ throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException,
+ RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException,
+ StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException {
+ Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone,
+ null, "123456");
+ return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash,
+ code.userInputCode, null).user;
+ }
+
+ @Test
+ public void testActiveUserIsRemovedAfterLinkingAccounts() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password");
+ AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com");
+
+ {
+ // Update active user
+ JsonObject responseBody = new JsonObject();
+ responseBody.addProperty("email", "test@example.com");
+ responseBody.addProperty("password", "password");
+
+ HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(),
+ "emailpassword");
+ }
+ {
+ JsonObject emailObject = new JsonObject();
+ emailObject.addProperty("id", "test@example.com");
+ emailObject.addProperty("isVerified", false);
+
+ JsonObject signUpRequestBody = new JsonObject();
+ signUpRequestBody.addProperty("thirdPartyId", "google");
+ signUpRequestBody.addProperty("thirdPartyUserId", "google-user");
+ signUpRequestBody.add("email", emailObject);
+
+ HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null,
+ SemVer.v4_0.get(), "thirdparty");
+ }
+
+ int userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000);
+ assertEquals(2, userCount);
+
+ {
+ // Link accounts
+ AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId());
+
+ JsonObject params = new JsonObject();
+ params.addProperty("recipeUserId", user1.getSupertokensUserId());
+ params.addProperty("primaryUserId", user2.getSupertokensUserId());
+
+ HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null,
+ WebserverAPI.getLatestCDIVersion().get(), "");
+ }
+
+ // we don't remove the active user for the recipe user, so it should still be 2
+ userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000);
+ assertEquals(2, userCount);
+
+ // Sign in to the accounts once again
+ {
+ // Update active user
+ JsonObject responseBody = new JsonObject();
+ responseBody.addProperty("email", "test@example.com");
+ responseBody.addProperty("password", "password");
+
+ HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(),
+ "emailpassword");
+ }
+ {
+ JsonObject emailObject = new JsonObject();
+ emailObject.addProperty("id", "test@example.com");
+ emailObject.addProperty("isVerified", false);
+
+ JsonObject signUpRequestBody = new JsonObject();
+ signUpRequestBody.addProperty("thirdPartyId", "google");
+ signUpRequestBody.addProperty("thirdPartyUserId", "google-user");
+ signUpRequestBody.add("email", emailObject);
+
+ HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null,
+ SemVer.v4_0.get(), "thirdparty");
+ }
+
+ // there should still be only one active user
+ userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000);
+ assertEquals(2, userCount);
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+}
diff --git a/src/test/java/io/supertokens/test/accountlinking/api/CanCreatePrimaryUserAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/CanCreatePrimaryUserAPITest.java
new file mode 100644
index 000000000..bf2d42429
--- /dev/null
+++ b/src/test/java/io/supertokens/test/accountlinking/api/CanCreatePrimaryUserAPITest.java
@@ -0,0 +1,372 @@
+/*
+ * 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 io.supertokens.test.accountlinking.api;
+
+import com.google.gson.JsonObject;
+import io.supertokens.ProcessState;
+import io.supertokens.authRecipe.AuthRecipe;
+import io.supertokens.emailpassword.EmailPassword;
+import io.supertokens.featureflag.EE_FEATURES;
+import io.supertokens.featureflag.FeatureFlagTestContent;
+import io.supertokens.pluginInterface.STORAGE_TYPE;
+import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
+import io.supertokens.storageLayer.StorageLayer;
+import io.supertokens.test.TestingProcessManager;
+import io.supertokens.test.Utils;
+import io.supertokens.test.httpRequest.HttpRequestForTesting;
+import io.supertokens.test.httpRequest.HttpResponseException;
+import io.supertokens.thirdparty.ThirdParty;
+import io.supertokens.useridmapping.UserIdMapping;
+import io.supertokens.webserver.WebserverAPI;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class CanCreatePrimaryUserAPITest {
+ @Rule
+ public TestRule watchman = Utils.getOnFailure();
+
+ @AfterClass
+ public static void afterTesting() {
+ Utils.afterTesting();
+ }
+
+ @Before
+ public void beforeEach() {
+ Utils.reset();
+ }
+
+ @Test
+ public void canCreateReturnsTrue() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234");
+
+ {
+ Map params = new HashMap<>();
+ params.put("recipeUserId", user.getSupertokensUserId());
+
+ JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null,
+ WebserverAPI.getLatestCDIVersion().get(), "");
+ assertEquals(2, response.entrySet().size());
+ assertEquals("OK", response.get("status").getAsString());
+ assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean());
+ }
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ {
+ Map params = new HashMap<>();
+ params.put("recipeUserId", user.getSupertokensUserId());
+
+ JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null,
+ WebserverAPI.getLatestCDIVersion().get(), "");
+ assertEquals(2, response.entrySet().size());
+ assertEquals("OK", response.get("status").getAsString());
+ assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean());
+ }
+
+ process.kill();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
+ }
+
+ @Test
+ public void canCreateReturnsTrueWithUserIdMapping() throws Exception {
+ String[] args = {"../"};
+ TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
+ FeatureFlagTestContent.getInstance(process.getProcess())
+ .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
+ EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
+ process.startProcess();
+ assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
+
+ if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
+ return;
+ }
+
+ AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234");
+ UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "r1", null, false);
+
+ {
+ Map params = new HashMap<>();
+ params.put("recipeUserId", "r1");
+
+ JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "",
+ "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null,
+ WebserverAPI.getLatestCDIVersion().get(), "");
+ assertEquals(2, response.entrySet().size());
+ assertEquals("OK", response.get("status").getAsString());
+ assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean());
+ }
+
+ AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId());
+
+ {
+ Map