diff --git a/CHANGELOG.md b/CHANGELOG.md index df836f1f..9788ba2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,106 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Support for MFA recipe +## [5.0.0] - 2023-09-19 + +### Changes + +- Support for Account Linking + - 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 + +1. Ensure that the core is already upgraded to the version 6.0.13 (CDI version 3.0) +2. Stop the core instance(s) +3. Run the migration script + ```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); + ``` +4. Run the new instance(s) of the core (version 7.0.0) + +## [4.0.2] + +- Fixes null pointer issue when user belongs to no tenant. + + +## [4.0.1] - 2023-07-11 + +- Fixes duplicate users in users search queries when user is associated to multiple tenants + + ## [4.0.0] - 2023-06-02 ### Changes diff --git a/build.gradle b/build.gradle index 1609748d..a3e8c53f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "4.0.0" +version = "5.0.0" repositories { mavenCentral() diff --git a/jar/postgresql-plugin-4.0.0.jar b/jar/postgresql-plugin-4.0.0.jar deleted file mode 100644 index 2a520c02..00000000 Binary files a/jar/postgresql-plugin-4.0.0.jar and /dev/null differ diff --git a/jar/postgresql-plugin-5.0.0.jar b/jar/postgresql-plugin-5.0.0.jar new file mode 100644 index 00000000..efa78f98 Binary files /dev/null and b/jar/postgresql-plugin-5.0.0.jar differ diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index 431c3b08..a5fdc62c 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/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 2d91c22b..9b4a1a9b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -23,13 +23,14 @@ import com.zaxxer.hikari.pool.HikariPool; 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; @@ -56,6 +57,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.*; @@ -78,6 +80,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; @@ -99,12 +102,15 @@ 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, DashboardSQLStorage, TOTPSQLStorage, MfaStorage, ActiveUsersStorage { + UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, MfaStorage, ActiveUsersStorage, AuthRecipeSQLStorage { // these configs are protected from being modified / viewed by the dev using the SuperTokens // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. @@ -147,7 +153,8 @@ public STORAGE_TYPE getType() { } @Override - public void loadConfig(JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException { + public void loadConfig(JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) + throws InvalidConfigException { Config.loadConfig(this, configJson, logLevels, tenantIdentifier); } @@ -650,6 +657,17 @@ public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, Tra } } + @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); + } + } + @Override public void setKeyValue_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String key, KeyValueInfo info) @@ -750,7 +768,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("This method is only for testing"); } @@ -819,7 +838,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); } @@ -853,7 +873,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 { @@ -886,28 +907,12 @@ 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) + public void deleteEmailPasswordUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) throws StorageQueryException { try { - return EmailPasswordQueries.getUserInfoUsingEmail(this, tenantIdentifier, email); + Connection sqlCon = (Connection) con.getConnection(); + EmailPasswordQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -918,7 +923,7 @@ public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetToke 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 PSQLException) { ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); @@ -1010,18 +1015,6 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio } } - @Override - public UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, - String userId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void deleteExpiredEmailVerificationTokens() throws StorageQueryException { try { @@ -1091,12 +1084,14 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans } @Override - public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId) + 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); } } @@ -1197,21 +1192,6 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException { } } - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction( - AppIdentifier appIdentifier, TransactionConnection con, - String thirdPartyId, - String thirdPartyUserId) - throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); - try { - return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, thirdPartyId, - thirdPartyUserId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String thirdPartyId, String thirdPartyUserId, @@ -1226,9 +1206,9 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction } @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 { @@ -1269,44 +1249,12 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( } @Override - public void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - try { - ThirdPartyQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId( - TenantIdentifier tenantIdentifier, String thirdPartyId, - String thirdPartyUserId) + public void deleteThirdPartyUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, + boolean deleteUserIdMappingToo) throws StorageQueryException { try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId, - thirdPartyUserId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier, - String id) - throws StorageQueryException { - try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, appIdentifier, id); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail( - TenantIdentifier tenantIdentifier, @NotNull String email) - throws StorageQueryException { - try { - return ThirdPartyQueries.getThirdPartyUsersByEmail(this, tenantIdentifier, email); + Connection sqlCon = (Connection) con.getConnection(); + ThirdPartyQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1412,9 +1360,11 @@ public int countUsersEnabledMfaAndActiveSince(AppIdentifier appIdentifier, long } @Override - public void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { - ActiveUsersQueries.deleteUserActive(this, appIdentifier, userId); + Connection sqlCon = (Connection) con.getConnection(); + ActiveUsersQueries.deleteUserActive_Transaction(sqlCon, this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1430,6 +1380,57 @@ public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) } } + @Override + public AuthRecipeUserInfo getPrimaryUserById(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + return GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public String getPrimaryUserIdStrForUserId(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + return GeneralQueries.getPrimaryUserIdStrForUserId(this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByEmail(TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException { + try { + 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); + } + } + @Override public List getJWTSigningKeys_Transaction(AppIdentifier appIdentifier, TransactionConnection con) @@ -1618,7 +1619,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 { @@ -1736,9 +1738,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 { @@ -1792,12 +1796,14 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde } @Override - public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws + public void deletePasswordlessUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) throws StorageQueryException { try { - PasswordlessQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); + Connection sqlCon = (Connection) con.getConnection(); + PasswordlessQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @@ -1872,37 +1878,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 { @@ -1925,7 +1900,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 { @@ -1944,6 +1920,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 { @@ -2066,17 +2053,20 @@ public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userI } @Override - public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws + public void deleteAllRolesForUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { - UserRolesQueries.deleteAllRolesForUser(this, appIdentifier, userId); + Connection sqlCon = (Connection) con.getConnection(); + UserRolesQueries.deleteAllRolesForUser_Transaction(sqlCon, 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(); @@ -2134,7 +2124,8 @@ 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 { @@ -2146,7 +2137,8 @@ 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 { @@ -2158,7 +2150,8 @@ 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 UserRolesQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role); @@ -2203,7 +2196,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, @@ -2217,7 +2211,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, @@ -2352,6 +2347,7 @@ public boolean deleteTenantInfoInBaseStorage(TenantIdentifier tenantIdentifier) public boolean deleteAppInfoInBaseStorage(AppIdentifier appIdentifier) throws StorageQueryException { return deleteTenantInfoInBaseStorage(appIdentifier.getAsPublicTenantIdentifier()); } + @Override public boolean deleteConnectionUriDomainInfoInBaseStorage(String connectionUriDomain) throws StorageQueryException { return deleteTenantInfoInBaseStorage(new TenantIdentifier(connectionUriDomain, null, null)); @@ -2363,67 +2359,57 @@ public TenantConfig[] getAllTenants() throws StorageQueryException { } @Override - public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) + public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId) throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { + 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()); - } + String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier, + userId); - 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!"); - } + if (recipeId == null) { + throw new UnknownUserIdException(); + } - sqlCon.commit(); - return added; - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); - } - }); - } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof SQLException) { - PostgreSQLConfig config = Config.getConfig(this); - ServerErrorMessage serverErrorMessage = ((PSQLException) e.actualException).getServerErrorMessage(); + 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!"); + } - if (isForeignKeyConstraintError(serverErrorMessage, config.getUsersTable(), "tenant_id")) { - throw new TenantOrAppNotFoundException(tenantIdentifier); - } - if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(), "email")) { - throw new DuplicateEmailException(); - } - if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(), "third_party_user_id")) { - throw new DuplicateThirdPartyUserException(); - } - if (isUniqueConstraintError(serverErrorMessage, - Config.getConfig(this).getPasswordlessUserToTenantTable(), "phone_number")) { - throw new DuplicatePhoneNumberException(); - } - if (isUniqueConstraintError(serverErrorMessage, - Config.getConfig(this).getPasswordlessUserToTenantTable(), "email")) { - throw new DuplicateEmailException(); - } + sqlCon.commit(); + return added; + } catch (SQLException throwables) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverErrorMessage = ((PSQLException) throwables).getServerErrorMessage(); - 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; + if (isForeignKeyConstraintError(serverErrorMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } - throw new StorageQueryException(e.actualException); + if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(), "email")) { + throw new DuplicateEmailException(); + } + if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(), + "third_party_user_id")) { + throw new DuplicateThirdPartyUserException(); + } + if (isUniqueConstraintError(serverErrorMessage, + Config.getConfig(this).getPasswordlessUserToTenantTable(), "phone_number")) { + throw new DuplicatePhoneNumberException(); + } + if (isUniqueConstraintError(serverErrorMessage, + Config.getConfig(this).getPasswordlessUserToTenantTable(), "email")) { + throw new DuplicateEmailException(); + } + + throw new StorageQueryException(throwables); } } @@ -2444,11 +2430,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!"); } @@ -2466,11 +2455,12 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String throw (StorageQueryException) e.actualException; } throw new StorageQueryException(e.actualException); - } + } } @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) { @@ -2520,7 +2510,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); @@ -2530,7 +2521,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(); @@ -2900,8 +2892,182 @@ public String[] getAllTablesInTheDatabaseThatHasDataForAppId(String appId) throw } } + @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 doesUserIdExist_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String externalUserId) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.doesUserIdExist_Transaction(this, sqlCon, appIdentifier, externalUserId); + } 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); + } + } + @TestOnly public Thread getMainThread() { return mainThread; } + + @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/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index 3d4745c3..d296e908 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -1,14 +1,14 @@ package io.supertokens.storage.postgresql.queries; -import java.math.BigInteger; -import java.sql.SQLException; - +import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; +import java.sql.Connection; +import java.sql.SQLException; + import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -21,9 +21,10 @@ static String getQueryToCreateUserLastActiveTable(Start start) { + "user_id VARCHAR(128)," + "last_active_time BIGINT," + "PRIMARY KEY(app_id, user_id)," - + "CONSTRAINT " + Utils.getConstraintName(schema, Config.getConfig(start).getUserLastActiveTable(), "app_id", "fkey") + + "CONSTRAINT " + + Utils.getConstraintName(schema, Config.getConfig(start).getUserLastActiveTable(), "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; } @@ -32,7 +33,8 @@ static String getQueryToCreateAppIdIndexForUserLastActiveTable(Start start) { + Config.getConfig(start).getUserLastActiveTable() + "(app_id);"; } - 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 >= ?"; @@ -47,7 +49,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 = ?"; @@ -61,11 +86,13 @@ 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 " - + "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 >= ?"; + 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 >= ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -110,9 +137,12 @@ public static int countUsersEnabledMfaAndActiveSince(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 -> { @@ -143,12 +173,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/storage/postgresql/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java index 81d869d6..55bb51c4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -17,8 +17,11 @@ package io.supertokens.storage.postgresql.queries; 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.DuplicateEmailException; +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 +35,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.EMAIL_PASSWORD; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -52,7 +56,8 @@ static String getQueryToCreateUsersTable(Start start) { + "time_joined BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, "user_id", "fkey") + " 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," + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUsersTable, null, "pkey") + " PRIMARY KEY (app_id, user_id)" + ");"; @@ -74,7 +79,8 @@ static String getQueryToCreateEmailPasswordUserToTenantTable(Start start) { + " PRIMARY KEY (app_id, tenant_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, emailPasswordUserToTenantTable, "user_id", "fkey") + " 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 } @@ -87,13 +93,15 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "token VARCHAR(128) NOT NULL" - + " CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + " UNIQUE," + + " CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "token", "key") + + " UNIQUE," + + "email VARCHAR(256)," // nullable cause of backwards compatibility. + "token_expiry BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, null, "pkey") + " PRIMARY KEY (app_id, user_id, token)," + "CONSTRAINT " + Utils.getConstraintName(schema, passwordResetTokensTable, "user_id", "fkey") + " FOREIGN KEY (app_id, user_id)" - + " REFERENCES " + Config.getConfig(start).getEmailPasswordUsersTable() + "(app_id, user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id)" + " ON DELETE CASCADE ON UPDATE CASCADE" + ");"; // @formatter:on @@ -115,7 +123,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 = ?"; @@ -127,7 +136,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() @@ -151,10 +161,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()); @@ -165,8 +177,9 @@ 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() - + " 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(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -189,8 +202,9 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans String userId) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() - + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -208,28 +222,12 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, AppIdentifier appIdentifier, - String id) + public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, + String token) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() - + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; - 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, userInfo); - } - - 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() - + " WHERE app_id = ? AND token = ?"; + 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()); pst.setString(2, token); @@ -241,43 +239,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(?, ?, ?, ?)"; - - update(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, tokenHash); - pst.setLong(4, expiry); - }); + 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); + 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); }); } @@ -306,57 +323,62 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, 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, userInfo); + } } - public static UserInfoPartial getUserInfoUsingId(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 + 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 = ?"; @@ -371,28 +393,55 @@ public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon, }); } - public static List getUsersInfoUsingIdList(Start start, 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 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 -> { - for (int i = 0; i < ids.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, 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()) { @@ -400,55 +449,111 @@ public static List getUsersInfoUsingIdList(Start start, List i } return finalResult; }); - return userInfoWithTenantIds(start, 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 = ? FOR UPDATE"; + + 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, 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); }); + + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), finalAccountLinkingInfo.primaryUserId); } { // 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 ON CONSTRAINT " + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getEmailPasswordUserToTenantTable(), null, "pkey") + + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -461,7 +566,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() @@ -477,42 +583,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, 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, 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) { + 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 List userInfoWithTenantIds(Start start, 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, userInfos); + 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 UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, 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, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, 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, 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.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 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(start, + appIdentifier, + userIds); + for (UserInfoPartial userInfo : userInfos) { + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + + return userInfos; } private static class UserInfoPartial { @@ -520,6 +687,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(); @@ -527,6 +697,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 { @@ -543,7 +720,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/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index afe360fb..86b9359d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -19,10 +19,8 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; 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.multitenancy.TenantIdentifierWithStorage; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -30,8 +28,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -52,7 +49,7 @@ static String getQueryToCreateEmailVerificationTable(Start start) { + " PRIMARY KEY (app_id, user_id, email)," + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTable, "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -71,13 +68,14 @@ static String getQueryToCreateEmailVerificationTokensTable(Start start) { + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id VARCHAR(128) NOT NULL," + "email VARCHAR(256) NOT NULL," - + "token VARCHAR(128) NOT NULL CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "token", "key") + " UNIQUE," + + "token VARCHAR(128) NOT NULL CONSTRAINT " + + Utils.getConstraintName(schema, emailVerificationTokensTable, "token", "key") + " UNIQUE," + "token_expiry BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, null, "pkey") + " PRIMARY KEY (app_id, tenant_id, user_id, email, token), " + "CONSTRAINT " + Utils.getConstraintName(schema, emailVerificationTokensTable, "tenant_id", "fkey") + " 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" + ")"; // @formatter:on } @@ -100,7 +98,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() @@ -124,8 +123,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 = ?"; @@ -137,7 +138,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 " @@ -155,7 +157,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(?, ?, ?, ?, ?, ?)"; @@ -173,10 +176,13 @@ 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 { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ? FOR UPDATE"; + + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ? FOR UPDATE"; return execute(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -199,9 +205,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()); @@ -233,38 +241,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/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 44e8cd24..8be26c54 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -20,6 +20,7 @@ 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; @@ -36,6 +37,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.storage.postgresql.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.CREATING_NEW_TABLE; @@ -72,13 +74,19 @@ 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," + "recipe_id VARCHAR(128) NOT NULL," + "time_joined BIGINT NOT NULL," + + "primary_or_recipe_user_time_joined BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") + " PRIMARY KEY (app_id, tenant_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "tenant_id", "fkey") + " FOREIGN KEY(app_id, tenant_id)" + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "primary_or_recipe_user_id", "fkey") + + " FOREIGN KEY(app_id, primary_or_recipe_user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + @@ -97,9 +105,44 @@ public static String getQueryToCreateTenantIdIndexForUsersTable(Start start) { + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id);"; } - 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);"; + 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() + + "(primary_or_recipe_user_id, app_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) { @@ -109,8 +152,8 @@ private static String getQueryToCreateAppsTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + appsTable + " (" + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + "created_at_time BIGINT ," - + "CONSTRAINT " + Utils.getConstraintName(schema, appsTable, null, "pkey") - + " PRIMARY KEY(app_id)" + + + "CONSTRAINT " + Utils.getConstraintName(schema, appsTable, null, "pkey") + + " PRIMARY KEY(app_id)" + " );"; // @formatter:on } @@ -169,8 +212,13 @@ private static String getQueryToCreateAppIdToUserIdTable(Start start) { + "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," + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, null, "pkey") + " PRIMARY KEY (app_id, user_id), " + + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "primary_or_recipe_user_id", "fkey") + + " FOREIGN KEY(app_id, primary_or_recipe_user_id)" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" @@ -223,7 +271,12 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto 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())) { @@ -231,7 +284,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, ActiveUsersQueries.getQueryToCreateUserLastActiveTable(start), NO_OP_SETTER); // Index - update(start, ActiveUsersQueries.getQueryToCreateAppIdIndexForUserLastActiveTable(start), NO_OP_SETTER); + update(start, ActiveUsersQueries.getQueryToCreateAppIdIndexForUserLastActiveTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { @@ -262,7 +316,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); // index - update(start, MultitenancyQueries.getQueryToCreateTenantIdIndexForTenantThirdPartyProvidersTable(start), + update(start, + MultitenancyQueries.getQueryToCreateTenantIdIndexForTenantThirdPartyProvidersTable(start), NO_OP_SETTER); } @@ -272,7 +327,9 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto NO_OP_SETTER); // index - update(start, MultitenancyQueries.getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable(start), + update(start, + MultitenancyQueries.getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable( + start), NO_OP_SETTER); } @@ -391,7 +448,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, UserRolesQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER); // index update(start, UserRolesQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER); - update(start, UserRolesQueries.getQueryToCreateRoleIndexForRolePermissionsTable(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateRoleIndexForRolePermissionsTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserRolesTable())) { @@ -409,7 +467,9 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, UserIdMappingQueries.getQueryToCreateUserIdMappingTable(start), NO_OP_SETTER); // index - update(start, UserIdMappingQueries.getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable(start), NO_OP_SETTER); + update(start, + UserIdMappingQueries.getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getDashboardUsersTable())) { @@ -417,7 +477,8 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto update(start, DashboardQueries.getQueryToCreateDashboardUsersTable(start), NO_OP_SETTER); // Index - update(start, DashboardQueries.getQueryToCreateAppIdIndexForDashboardUsersTable(start), NO_OP_SETTER); + update(start, DashboardQueries.getQueryToCreateAppIdIndexForDashboardUsersTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getDashboardSessionsTable())) { @@ -609,7 +670,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 ("); @@ -641,7 +704,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 ("); @@ -674,7 +738,8 @@ 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 = ?"; return execute(start, QUERY, pst -> { @@ -683,15 +748,33 @@ public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, }, ResultSet::next); } - public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) + 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 = ?"; + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, 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); } @@ -703,7 +786,7 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant 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<>(); @@ -717,6 +800,7 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant + " AS allAuthUsersTable" + " JOIN " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS emailpasswordTable ON allAuthUsersTable.app_id = emailpasswordTable.app_id AND " + + "allAuthUsersTable.tenant_id = emailpasswordTable.tenant_id AND " + "allAuthUsersTable.user_id = emailpasswordTable.user_id"; // attach email tags to queries @@ -742,15 +826,16 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant // check if we should search through the thirdparty table if (dashboardSearchTags.shouldThirdPartyTableBeSearched()) { String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() - + " AS allAuthUsersTable" + - " JOIN " + getConfig(start).getThirdPartyUsersTable() - + " AS thirdPartyTable ON allAuthUsersTable.app_id = thirdPartyTable.app_id AND" - + " allAuthUsersTable.user_id = thirdPartyTable.user_id" + + " AS allAuthUsersTable" + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + - " AS thirdPartyToTenantTable ON thirdPartyTable.app_id = thirdPartyToTenantTable" + + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable" + ".app_id AND" - + " thirdPartyTable.user_id = thirdPartyToTenantTable.user_id"; + + " allAuthUsersTable.tenant_id = thirdPartyToTenantTable.tenant_id AND" + + " allAuthUsersTable.user_id = thirdPartyToTenantTable.user_id" + + " JOIN " + getConfig(start).getThirdPartyUsersTable() + + " AS thirdPartyTable ON thirdPartyToTenantTable.app_id = thirdPartyTable.app_id AND" + + " thirdPartyToTenantTable.user_id = thirdPartyTable.user_id"; // check if email tag is present if (dashboardSearchTags.emails != null) { @@ -815,6 +900,7 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant + " AS allAuthUsersTable" + " JOIN " + getConfig(start).getPasswordlessUserToTenantTable() + " AS passwordlessTable ON allAuthUsersTable.app_id = passwordlessTable.app_id AND" + + " allAuthUsersTable.tenant_id = passwordlessTable.tenant_id AND" + " allAuthUsersTable.user_id = passwordlessTable.user_id"; // check if email tag is present @@ -874,22 +960,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 { @@ -913,11 +997,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++) { @@ -933,21 +1017,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++) { @@ -960,75 +1043,507 @@ 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); + }); + } + + updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, appIdentifier, 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); + }); + } + + updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, appIdentifier, 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.lockPhoneAndTenant_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.lockThirdPartyInfoAndTenant_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"); } - userIdList.add(user.userId); - recipeIdToUserIdListMap.put(recipeId, userIdList); + 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); + } - AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()]; + 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); + } - // we give the userId[] for each recipe to fetch all those user's details - for (RECIPE_ID recipeId : recipeIdToUserIdListMap.keySet()) { - List users = getUserInfoForRecipeIdFromUserIds(start, - tenantIdentifier, recipeId, recipeIdToUserIdListMap.get(recipeId)); + private static List getPrimaryUserInfoForUserIds(Start start, + AppIdentifier appIdentifier, + List userIds) + throws StorageQueryException, SQLException { + if (userIds.size() == 0) { + return new ArrayList<>(); + } - // 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); + // 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)); } - for (int i = 0; i < usersFromQuery.size(); i++) { - if (finalResult[i] == null) { - finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i).userId); - } + // 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); } - return finalResult; + 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); + } + + 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()); } - private static List getUserInfoForRecipeIdFromUserIds(Start start, - TenantIdentifier tenantIdentifier, - 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, userIds); - } else if (recipeId == RECIPE_ID.THIRD_PARTY) { - return ThirdPartyQueries.getUsersInfoUsingIdList(start, userIds); - } else if (recipeId == RECIPE_ID.PASSWORDLESS) { - return PasswordlessQueries.getUsersByIdList(start, 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 { + String QUERY = "SELECT recipe_id FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; - return execute(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, userId); @@ -1040,7 +1555,9 @@ public static String getRecipeIdForUser_Transaction(Start start, Connection sqlC }); } - public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, 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 " @@ -1054,22 +1571,67 @@ public static Map> getTenantIdsForUserIds_transaction(Start QUERY.append(","); } } - QUERY.append(")"); + QUERY.append(") AND app_id = ?"); return execute(sqlCon, QUERY.toString(), pst -> { for (int i = 0; i < userIds.length; i++) { - // i+1 cause this starts with 1 and not 0 + // 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) { + finalResult.put(userId, new ArrayList<>()); + } + while (result.next()) { String userId = result.getString("user_id").trim(); String tenantId = result.getString("tenant_id"); - if (!finalResult.containsKey(userId)) { - finalResult.put(userId, new ArrayList<>()); - } finalResult.get(userId).add(tenantId); } return finalResult; @@ -1109,12 +1671,13 @@ public static String[] getAllTablesInTheDatabaseThatHasDataForAppId(Start start, List result = new ArrayList<>(); for (String tableName : tableNames) { - String QUERY = "SELECT 1 FROM " + Config.getConfig(start).getTableSchema() + "." + tableName + " WHERE app_id = ?"; + String QUERY = + "SELECT 1 FROM " + Config.getConfig(start).getTableSchema() + "." + tableName + " WHERE app_id = ?"; boolean hasRows = execute(start, QUERY, pst -> { pst.setString(1, appId); }, res -> { - return res.next(); + return res.next(); }); if (hasRows) { result.add(tableName); @@ -1124,13 +1687,87 @@ 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"; - UserInfoPaginationResultHolder(String userId, String recipeId) { - this.userId = userId; - this.recipeId = recipeId; + 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 void updateTimeJoinedForPrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String primaryUserId) + throws SQLException, StorageQueryException { + 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); + }); + } + + 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; } } @@ -1149,4 +1786,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/storage/postgresql/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java index 0d03bc81..31858944 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -17,13 +17,15 @@ package io.supertokens.storage.postgresql.queries; 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 io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; @@ -36,6 +38,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.PASSWORDLESS; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -55,7 +58,8 @@ public static String getQueryToCreateUsersTable(Start start) { + "time_joined BIGINT NOT NULL, " + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + " 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," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, null, "pkey") + " PRIMARY KEY (app_id, user_id)" + ");"; @@ -79,7 +83,8 @@ static String getQueryToCreatePasswordlessUserToTenantTable(Start start) { + " PRIMARY KEY (app_id, tenant_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, passwordlessUserToTenantTable, "user_id", "fkey") + " 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 } @@ -98,7 +103,7 @@ public static String getQueryToCreateDevicesTable(Start start) { + "failed_attempts INT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, devicesTable, "tenant_id", "fkey") + " 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," + "CONSTRAINT " + Utils.getConstraintName(schema, devicesTable, null, "pkey") + " PRIMARY KEY (app_id, tenant_id, device_id_hash)" + ");"; @@ -126,7 +131,8 @@ public static String getQueryToCreateCodesTable(Start start) { + " PRIMARY KEY (app_id, tenant_id, code_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, codesTable, "device_id_hash", "fkey") + " FOREIGN KEY (app_id, tenant_id, device_id_hash)" - + " REFERENCES " + Config.getConfig(start).getPasswordlessDevicesTable() + "(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" + ");"; } @@ -138,7 +144,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) { @@ -151,8 +158,10 @@ public static String getQueryToCreateCodeCreatedAtIndex(Start start) { + Config.getConfig(start).getPasswordlessCodesTable() + "(app_id, tenant_id, created_at);"; } - 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 { @@ -197,7 +206,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" @@ -210,7 +220,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 = ?"; @@ -221,7 +232,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() @@ -234,7 +247,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() @@ -251,7 +265,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() @@ -264,7 +279,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() @@ -281,7 +297,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)" @@ -311,7 +328,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 " @@ -335,7 +354,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 " @@ -354,7 +375,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 = ?"; @@ -366,30 +388,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); }); } @@ -417,16 +444,20 @@ public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier pst.setString(5, phoneNumber); }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, 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 " @@ -453,49 +484,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() @@ -519,7 +560,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() @@ -562,7 +604,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() @@ -609,7 +652,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. @@ -617,7 +661,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 < ?"; @@ -639,7 +684,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 = ?"; @@ -656,7 +702,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. @@ -664,27 +711,52 @@ public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifi } } - public static List getUsersByIdList(Start start, 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 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 -> { - for (int i = 0; i < ids.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, 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()) { @@ -692,16 +764,22 @@ public static List getUsersByIdList(Start start, List ids) } return finalResult; }); - return userInfoWithTenantIds(start, 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 -> { @@ -710,92 +788,166 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str } return null; }); - return userInfoWithTenantIds(start, userInfo); } - public static UserInfoPartial getUserById(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { - // 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, 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 { + // 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 FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND email = ? FOR UPDATE"; - return execute(sqlCon, QUERY, pst -> { + return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); + pst.setString(2, email); }, 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 List lockPhoneAndTenant_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String phoneNumber) + throws SQLException, StorageQueryException { + + String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND phone_number = ? FOR UPDATE"; + 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 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, 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 = ?"; + + 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; + }); + } - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + 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, 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); }); + + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), accountLinkingInfo.primaryUserId); } { // 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 ON CONSTRAINT " + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getPasswordlessUserToTenantTable(), null, "pkey") + + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -809,7 +961,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() @@ -827,42 +980,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, 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, 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, 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, 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, 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, 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, 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, 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 { @@ -905,6 +1140,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(); @@ -917,6 +1155,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 { @@ -936,7 +1181,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/storage/postgresql/queries/SessionQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java index 928fbd66..d6685638 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -124,19 +124,44 @@ public static void createNewSession(Start start, TenantIdentifier tenantIdentifi public static SessionInfo getSessionInfo_Transaction(Start start, Connection con, 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 = ? FOR UPDATE"; - return execute(con, QUERY, pst -> { + // 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 = ? FOR UPDATE"; + 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, @@ -208,6 +233,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() @@ -311,16 +348,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; }); @@ -372,7 +417,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() { @@ -382,14 +427,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/storage/postgresql/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java index dc56177f..2a37c9dc 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -17,21 +17,25 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.RowMapper; +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.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 io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; -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.pluginInterface.RECIPE_ID.THIRD_PARTY; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @@ -53,7 +57,8 @@ static String getQueryToCreateUsersTable(Start start) { + "time_joined BIGINT NOT NULL," + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, "user_id", "fkey") + " 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," + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUsersTable, null, "pkey") + " PRIMARY KEY (app_id, user_id)" + ");"; @@ -80,41 +85,48 @@ static String getQueryToCreateThirdPartyUserToTenantTable(Start start) { + "user_id CHAR(36) NOT NULL," + "third_party_id VARCHAR(28) NOT NULL," + "third_party_user_id VARCHAR(256) NOT NULL," - + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, "third_party_user_id", "key") + + "CONSTRAINT " + + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, "third_party_user_id", "key") + " UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, null, "pkey") + " PRIMARY KEY (app_id, tenant_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, thirdPartyUserToTenantTable, "user_id", "fkey") + " 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); }); } @@ -145,9 +157,11 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, 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); @@ -155,69 +169,125 @@ 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 { + String QUERY = "SELECT tp.user_id as user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " WHERE tp.app_id = ? AND tp.email = ? FOR UPDATE"; + + 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 lockThirdPartyInfoAndTenant_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 = ?"; + String QUERY = "SELECT user_id " + + " FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ? FOR UPDATE"; - 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, userInfo); } - public static List getUsersInfoUsingIdList(Start start, 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 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 -> { - for (int i = 0; i < ids.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, 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()) { @@ -225,36 +295,82 @@ public static List getUsersInfoUsingIdList(Start start, List i } return finalResult; }); - return userInfoWithTenantIds(start, 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, userInfo); } public static void updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @@ -271,32 +387,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 { - - 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 = ? FOR UPDATE"; - 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, 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 FOR UPDATE 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 = ?"; @@ -311,55 +407,87 @@ 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, 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); }); + + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), accountLinkingInfo.primaryUserId); } { // 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 ON CONSTRAINT " + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getThirdPartyUserToTenantTable(), null, "pkey") + + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); @@ -372,7 +500,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() @@ -390,56 +519,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, 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, Arrays.asList(userInfo)).get(0); - } + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds(Start start, 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, 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, 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, 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, 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, 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 { @@ -455,7 +612,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/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index cc600818..a32dccb7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -25,6 +25,7 @@ import io.supertokens.storage.postgresql.utils.Utils; import javax.annotation.Nullable; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -216,6 +217,56 @@ 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; + }); + } + + 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); + }); + } + private static class UserIdMappingRowMapper implements RowMapper { private static final UserIdMappingRowMapper INSTANCE = new UserIdMappingRowMapper(); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java index d645bad1..1d2b6231 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java @@ -18,7 +18,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; - import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; @@ -46,7 +45,7 @@ public static String getQueryToCreateUserMetadataTable(Start start) { + " PRIMARY KEY(app_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -56,7 +55,8 @@ public static String getQueryToCreateAppIdIndexForUserMetadataTable(Start start) + Config.getConfig(start).getUserMetadataTable() + "(app_id);"; } - 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 = ?"; @@ -66,7 +66,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() @@ -97,7 +110,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/storage/postgresql/queries/UserRolesQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java index 3069faa6..549cac86 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java @@ -19,7 +19,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; @@ -45,7 +44,7 @@ public static String getQueryToCreateRolesTable(Start start) { + " PRIMARY KEY(app_id, role)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -103,11 +102,13 @@ public static String getQueryToCreateUserRolesTable(Start start) { } public static String getQueryToCreateTenantIdIndexForUserRolesTable(Start start) { - return "CREATE INDEX IF NOT EXISTS user_roles_tenant_id_index ON " + getConfig(start).getUserRolesTable() + "(app_id, tenant_id);"; + return "CREATE INDEX IF NOT EXISTS user_roles_tenant_id_index ON " + getConfig(start).getUserRolesTable() + + "(app_id, tenant_id);"; } public static String getQueryToCreateRoleIndexForUserRolesTable(Start start) { - return "CREATE INDEX IF NOT EXISTS user_roles_app_id_role_index ON " + getConfig(start).getUserRolesTable() + "(app_id, role);"; + return "CREATE INDEX IF NOT EXISTS user_roles_app_id_role_index ON " + getConfig(start).getUserRolesTable() + + "(app_id, role);"; } public static String getQueryToCreateUserRolesRoleIndex(Start start) { @@ -116,7 +117,7 @@ public static String getQueryToCreateUserRolesRoleIndex(Start start) { } public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String role) + AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getRolesTable() + "(app_id, role) VALUES (?, ?) ON CONFLICT DO NOTHING;"; @@ -129,7 +130,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"; @@ -140,7 +142,8 @@ public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start star }); } - public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + 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 -> { @@ -149,7 +152,8 @@ public static boolean deleteRole(Start start, AppIdentifier appIdentifier, Strin }) == 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 -> { @@ -158,7 +162,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 -> { @@ -173,7 +178,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<>(); @@ -247,7 +253,8 @@ 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 { String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ? AND role = ? FOR UPDATE"; @@ -257,7 +264,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 -> { @@ -275,7 +283,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 = ? "; @@ -323,7 +332,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 -> { @@ -333,10 +343,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/storage/postgresql/utils/Utils.java b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java index 7db2d0a5..91a58735 100644 --- a/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java +++ b/src/main/java/io/supertokens/storage/postgresql/utils/Utils.java @@ -42,4 +42,15 @@ public static String getConstraintName(String schema, String prefixedTableName, constraintName.append('_').append(typeSuffix); return constraintName.toString(); } + + 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/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java new file mode 100644 index 00000000..4f26a52c --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/AccountLinkingTests.java @@ -0,0 +1,154 @@ +/* + * 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.storage.postgresql.test; + +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.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storage.postgresql.test.httpRequest.HttpRequestForTesting; +import io.supertokens.storage.postgresql.test.httpRequest.HttpResponseException; +import io.supertokens.storageLayer.StorageLayer; +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.assertNotNull; + +public class AccountLinkingTests { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void canLinkFailsIfTryingToLinkUsersAcrossDifferentStorageLayers() 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; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 2); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, null, "t1"); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ) + ); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipe.createPrimaryUser(process.main, user1.getSupertokensUserId()); + + AuthRecipeUserInfo user2 = EmailPassword.signUp( + tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + process.getProcess(), "test2@example.com", "abcd1234"); + + try { + Map params = new HashMap<>(); + params.put("recipeUserId", user2.getSupertokensUserId()); + params.put("primaryUserId", user1.getSupertokensUserId()); + + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Cannot link users that are parts of different " + + "databases. Different pool IDs: |localhost|5432|supertokens|public AND " + + "|localhost|5432|st2|public")); + } + + + coreConfig = new JsonObject(); + coreConfig.addProperty("postgresql_connection_pool_size", 11); + + tenantIdentifier = new TenantIdentifier(null, null, "t2"); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ) + ); + + AuthRecipeUserInfo user3 = EmailPassword.signUp( + tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + process.getProcess(), "test2@example.com", "abcd1234"); + + Map params = new HashMap<>(); + params.put("recipeUserId", user3.getSupertokensUserId()); + params.put("primaryUserId", user1.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (response.get("status").getAsString().equals("OK")); + assert (!response.get("accountsAlreadyLinked").getAsBoolean()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 15d8016b..3dd0241f 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -18,9 +18,14 @@ package io.supertokens.storage.postgresql.test; 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.passwordless.Passwordless; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -599,6 +604,94 @@ public void testConcurrentDeleteAndInsert() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + @Test + public void testLinkAccountsInParallel() 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)); + + ExecutorService es = Executors.newFixedThreadPool(1000); + + AtomicBoolean pass = new AtomicBoolean(true); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + + for (int i = 0; i < 3000; i++) { + es.execute(() -> { + try { + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + AuthRecipe.unlinkAccounts(process.getProcess(), user2.getSupertokensUserId()); + } catch (Exception e) { + if (e.getMessage().toLowerCase().contains("the transaction might succeed if retried")) { + pass.set(false); + } + } + }); + } + + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); + + assert (pass.get()); + assertNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + assertNotNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testCreatePrimaryInParallel() 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)); + + ExecutorService es = Executors.newFixedThreadPool(1000); + + AtomicBoolean pass = new AtomicBoolean(true); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + + for (int i = 0; i < 3000; i++) { + es.execute(() -> { + try { + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.unlinkAccounts(process.getProcess(), user1.getSupertokensUserId()); + } catch (Exception e) { + if (e.getMessage().toLowerCase().contains("the transaction might succeed if retried")) { + pass.set(false); + } + } + }); + } + + es.shutdown(); + es.awaitTermination(2, TimeUnit.MINUTES); + + assert (pass.get()); + assertNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + assertNotNull(process + .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } /* diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java index fb5130a8..7fd988ac 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ExceptionParsingTest.java @@ -87,7 +87,7 @@ public void thirdPartySignupExceptions() throws Exception { String thirdPartyUserId = "tp_userId"; String userEmail = "useremail@asdf.fdas"; - var tp = new io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty(tpId, thirdPartyUserId); + var tp = new io.supertokens.pluginInterface.authRecipe.LoginMethod.ThirdParty(tpId, thirdPartyUserId); storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, tp, System.currentTimeMillis()); try { @@ -128,15 +128,18 @@ public void emailPasswordSignupExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected } try { - storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected @@ -171,8 +174,10 @@ public void updateUsersEmail_TransactionExceptions() String userEmail2 = "useremail2@asdf.fdas"; String userEmail3 = "useremail3@asdf.fdas"; - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); - storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail2, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail2, pwHash, + System.currentTimeMillis()); storage.startTransaction(conn -> { try { storage.updateUsersEmail_Transaction(new AppIdentifier(null, null), conn, userId, userEmail2); @@ -211,7 +216,8 @@ public void updateIsEmailVerified_TransactionExceptions() TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess()); + EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage( + process.getProcess()); String userId = "userId"; String userEmail = "useremail@asdf.fdas"; @@ -219,8 +225,9 @@ public void updateIsEmailVerified_TransactionExceptions() storage.startTransaction(conn -> { try { storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, - true); - storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, true); + true); + storage.updateIsEmailVerified_Transaction(new AppIdentifier(null, null), conn, userId, userEmail, + true); } catch (TenantOrAppNotFoundException e) { throw new RuntimeException(e); } @@ -280,11 +287,12 @@ public void addPasswordResetTokenExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000); + var info = new PasswordResetTokenInfo(userId, tokenHash, System.currentTimeMillis() + 10000, userEmail); try { storage.addPasswordResetToken(new AppIdentifier(null, null), info); } catch (UnknownUserIdException ex) { - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); } storage.addPasswordResetToken(new AppIdentifier(null, null), info); try { @@ -306,7 +314,8 @@ public void addEmailVerificationTokenExceptions() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess()); + EmailVerificationSQLStorage storage = (EmailVerificationSQLStorage) StorageLayer.getStorage( + process.getProcess()); String userId = "userId"; String tokenHash = "fakehash"; @@ -340,16 +349,19 @@ public void verifyEmailExceptions() throws Exception { String pwHash = "fakehash"; String userEmail = "useremail@asdf.fdas"; - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); try { - storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateUserIdException ex) { // expected } try { - storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, System.currentTimeMillis()); + storage.signUp(new TenantIdentifier(null, null, null), userId2, userEmail, pwHash, + System.currentTimeMillis()); throw new Exception("This should throw"); } catch (DuplicateEmailException ex) { // expected diff --git a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java index 7f39a6ca..b48324bb 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -1,7 +1,14 @@ package io.supertokens.storage.postgresql.test; 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.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -14,13 +21,14 @@ import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; +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; +import static org.junit.Assert.*; public class StorageLayerTest { @@ -94,4 +102,52 @@ public void totpCodeLengthTest() throws Exception { insertUsedCodeUtil(storage, code); } + @Test + public void testLinkedAccountUser() 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)); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid", "test2@example.com").user; + Thread.sleep(50); + Passwordless.CreateCodeResponse code1 = Passwordless.createCode(process.getProcess(), "test3@example.com", null, null, null); + AuthRecipeUserInfo user3 = Passwordless.consumeCode(process.getProcess(), code1.deviceId, code1.deviceIdHash, code1.userInputCode, null).user; + Thread.sleep(50); + Passwordless.CreateCodeResponse code2 = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + AuthRecipeUserInfo user4 = Passwordless.consumeCode(process.getProcess(), code2.deviceId, code2.deviceIdHash, code2.userInputCode, null).user; + + AuthRecipe.createPrimaryUser(process.getProcess(), user3.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user1.getSupertokensUserId(), user3.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user3.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user4.getSupertokensUserId(), user3.getSupertokensUserId()); + + String[] userIds = new String[]{ + user1.getSupertokensUserId(), + user2.getSupertokensUserId(), + user3.getSupertokensUserId(), + user4.getSupertokensUserId() + }; + + for (String userId : userIds){ + AuthRecipeUserInfo primaryUser = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())).getPrimaryUserById( + new AppIdentifier(null, null), userId); + assertEquals(user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + assertEquals(4, primaryUser.loginMethods.length); + assertTrue(primaryUser.loginMethods[0].timeJoined < primaryUser.loginMethods[1].timeJoined); + assertTrue(primaryUser.loginMethods[1].timeJoined < primaryUser.loginMethods[2].timeJoined); + assertTrue(primaryUser.loginMethods[2].timeJoined < primaryUser.loginMethods[3].timeJoined); + assertEquals(primaryUser.timeJoined, primaryUser.loginMethods[0].timeJoined); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java index e8c7493f..5a1d7a1f 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestUserPoolIdChangeBehaviour.java @@ -25,7 +25,7 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; @@ -91,7 +91,7 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); - UserInfo userInfo = EmailPassword.signUp( + AuthRecipeUserInfo userInfo = EmailPassword.signUp( tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("postgresql_host", "127.0.0.1"); @@ -108,7 +108,8 @@ public void testUsersWorkAfterUserPoolIdChanges() throws Exception { String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); assertNotEquals(userPoolId, userPoolId2); - UserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), + "user@example.com", "password"); assertEquals(userInfo, user2); @@ -134,7 +135,7 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti String userPoolId = tenantIdentifierWithStorage.getStorage().getUserPoolId(); - UserInfo userInfo = EmailPassword.signUp( + AuthRecipeUserInfo userInfo = EmailPassword.signUp( tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); coreConfig.addProperty("postgresql_host", "127.0.0.1"); @@ -157,7 +158,9 @@ public void testUsersWorkAfterUserPoolIdChangesAndServerRestart() throws Excepti String userPoolId2 = tenantIdentifierWithStorage.getStorage().getUserPoolId(); assertNotEquals(userPoolId, userPoolId2); - UserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), "user@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signIn(tenantIdentifierWithStorage, process.getProcess(), + "user@example.com", + "password"); assertEquals(userInfo, user2); }