diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index 00567642..bcba951d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -93,6 +93,7 @@ private synchronized void initialiseHikariDataSource() throws SQLException, Stor config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); config.addDataSourceProperty("tcpKeepAlive", "true"); + config.addDataSourceProperty("socketTimeout", "60"); // TODO: set maxLifetimeValue to lesser than 10 mins so that the following error doesnt happen: // io.supertokens.storage.postgresql.HikariLoggingAppender.doAppend(HikariLoggingAppender.java:117) | // SuperTokens diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 7811ebff..dfce1b43 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -48,6 +48,7 @@ import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.ConfigFieldInfo; import io.supertokens.pluginInterface.KeyValueInfo; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.STORAGE_TYPE; @@ -1417,10 +1418,33 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio UnknownUserIdException { Connection sqlCon = (Connection) conn.getConnection(); try { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // Acquire lock to get LockedUser for the new API LockedUser lockedUser = UserLockingQueries.lockUser(this, sqlCon, appIdentifier, userId); - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, - ACCOUNT_INFO_TYPE.EMAIL, email); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, + ACCOUNT_INFO_TYPE.EMAIL, email); + } else { + // In legacy mode, AccountInfoQueries is not used, so we need to check for + // email conflicts with other primary users manually + AuthRecipeUserInfo user = GeneralQueries.getPrimaryUserInfoForUserId_Transaction(this, sqlCon, + appIdentifier, userId); + if (user != null && user.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + AuthRecipeUserInfo[] existingUsers = GeneralQueries.listPrimaryUsersByEmail_legacy(this, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), tenantId), + email); + for (AuthRecipeUserInfo existingUser : existingUsers) { + if (existingUser.isPrimaryUser + && !existingUser.getSupertokensUserId().equals(user.getSupertokensUserId()) + && existingUser.tenantIds.contains(tenantId)) { + throw new EmailChangeNotAllowedException(); + } + } + } + } + } EmailPasswordQueries.updateUsersEmail_Transaction(this, sqlCon, appIdentifier, userId, email); } catch (SQLException e) { if (e instanceof PSQLException && isUniqueConstraintError(((PSQLException) e).getServerErrorMessage(), @@ -1685,10 +1709,33 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction UnknownUserIdException { Connection sqlCon = (Connection) con.getConnection(); try { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // Acquire lock to get LockedUser for the new API LockedUser lockedUser = UserLockingQueries.lockUser(this, sqlCon, appIdentifier, userId); - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, - ACCOUNT_INFO_TYPE.EMAIL, newEmail); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, + ACCOUNT_INFO_TYPE.EMAIL, newEmail); + } else { + // In legacy mode, AccountInfoQueries is not used, so we need to check for + // email conflicts with other primary users manually (app-scoped to handle cross-tenant linking) + AuthRecipeUserInfo user = GeneralQueries.getPrimaryUserInfoForUserId_Transaction(this, sqlCon, + appIdentifier, userId); + if (user != null && user.isPrimaryUser) { + AuthRecipeUserInfo[] existingUsers = GeneralQueries.listPrimaryUsersByEmail_legacy_forApp(this, + appIdentifier, newEmail); + for (AuthRecipeUserInfo existingUser : existingUsers) { + if (existingUser.isPrimaryUser + && !existingUser.getSupertokensUserId().equals(user.getSupertokensUserId())) { + // Check if they share any tenant + for (String tenantId : user.tenantIds) { + if (existingUser.tenantIds.contains(tenantId)) { + throw new EmailChangeNotAllowedException(); + } + } + } + } + } + } ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, thirdPartyId, thirdPartyUserId, newEmail); } catch (PhoneNumberChangeNotAllowedException | DuplicatePhoneNumberException | DuplicateThirdPartyUserException e) { @@ -2432,12 +2479,18 @@ public void updateUserEmailAndPhone_Transaction(AppIdentifier appIdentifier, Tra try { Connection sqlCon = (Connection) con.getConnection(); + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // Acquire lock once to get LockedUser for all calls LockedUser lockedUser = UserLockingQueries.lockUser(this, sqlCon, appIdentifier, userId); // Update non-nulls first if (email != null) { - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.EMAIL, email); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.EMAIL, email); + } else { + // Legacy email conflict check + checkLegacyEmailConflict(appIdentifier, userId, email); + } int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, userId, email); if (updated_rows != 1) { @@ -2445,7 +2498,12 @@ public void updateUserEmailAndPhone_Transaction(AppIdentifier appIdentifier, Tra } } if (phoneNumber != null) { - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.PHONE_NUMBER, phoneNumber); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.PHONE_NUMBER, phoneNumber); + } else { + // Legacy phone conflict check + checkLegacyPhoneConflict(appIdentifier, userId, phoneNumber); + } int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, appIdentifier, userId, phoneNumber); if (updated_rows != 1) { @@ -2455,7 +2513,9 @@ public void updateUserEmailAndPhone_Transaction(AppIdentifier appIdentifier, Tra // now update the nulls if (email == null && shouldUpdateEmail) { - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.EMAIL, email); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.EMAIL, email); + } int updated_rows = PasswordlessQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, userId, email); if (updated_rows != 1) { @@ -2463,7 +2523,9 @@ public void updateUserEmailAndPhone_Transaction(AppIdentifier appIdentifier, Tra } } if (phoneNumber == null && shouldUpdatePhoneNumber) { - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.PHONE_NUMBER, phoneNumber); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, appIdentifier, lockedUser, ACCOUNT_INFO_TYPE.PHONE_NUMBER, phoneNumber); + } int updated_rows = PasswordlessQueries.updateUserPhoneNumber_Transaction(this, sqlCon, appIdentifier, userId, phoneNumber); if (updated_rows != 1) { @@ -3147,7 +3209,10 @@ public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, // Get recipe ID from LockedUser (fetched from app_id_to_user_id during lock acquisition) String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(lockedUser); - AccountInfoQueries.addTenantIdToRecipeUser_Transaction(this, sqlCon, tenantIdentifier, lockedUser); + MigrationMode mode = Config.getConfig(Start.this).getMigrationMode(); + if (mode.writesToNewTables()) { + AccountInfoQueries.addTenantIdToRecipeUser_Transaction(this, sqlCon, tenantIdentifier, lockedUser); + } boolean added; if (recipeId.equals("emailpassword")) { @@ -3225,8 +3290,11 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String } else { throw new IllegalStateException("Should never come here!"); } - AccountInfoQueries.removeAccountInfoReservationForPrimaryUserWhileRemovingTenant_Transaction(this, sqlCon, tenantIdentifier, lockedUser); - AccountInfoQueries.removeAccountInfoForRecipeUserWhileRemovingTenant_Transaction(this, sqlCon, tenantIdentifier, lockedUser); + MigrationMode mode = Config.getConfig(Start.this).getMigrationMode(); + if (mode.writesToNewTables()) { + AccountInfoQueries.removeAccountInfoReservationForPrimaryUserWhileRemovingTenant_Transaction(this, sqlCon, tenantIdentifier, lockedUser); + AccountInfoQueries.removeAccountInfoForRecipeUserWhileRemovingTenant_Transaction(this, sqlCon, tenantIdentifier, lockedUser); + } sqlCon.commit(); return removed; @@ -3748,9 +3816,35 @@ public boolean makePrimaryUser_Transaction(AppIdentifier appIdentifier, Transact throw new UnknownUserIdException(); } + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // Use the LockedUser version of addPrimaryUserAccountInfo_Transaction - boolean didBecomePrimary = AccountInfoQueries.addPrimaryUserAccountInfo_Transaction( - this, sqlCon, appIdentifier, lockedUser); + boolean didBecomePrimary; + if (mode.writesToNewTables()) { + didBecomePrimary = AccountInfoQueries.addPrimaryUserAccountInfo_Transaction( + this, sqlCon, appIdentifier, lockedUser); + } else { + // In LEGACY mode, check if user is already primary + AuthRecipeUserInfo existingUser = GeneralQueries.getPrimaryUserInfoForUserId_Transaction( + this, sqlCon, appIdentifier, userId); + if (existingUser == null) { + throw new UnknownUserIdException(); + } + if (existingUser.isPrimaryUser && existingUser.getSupertokensUserId().equals(userId)) { + didBecomePrimary = false; // already primary + } else if (existingUser.isPrimaryUser) { + throw new CannotBecomePrimarySinceRecipeUserIdAlreadyLinkedWithPrimaryUserIdException( + existingUser.getSupertokensUserId(), + "This user ID is already linked to another user ID"); + } else { + // Check for conflicting account info + CanBecomePrimaryResult checkResult = checkIfLoginMethodCanBecomePrimary_legacy(appIdentifier, userId); + if (checkResult.status == CanBecomePrimaryResult.RESULT.CONFLICTING_ACCOUNT_INFO) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + checkResult.conflictingPrimaryUserId, checkResult.message); + } + didBecomePrimary = true; + } + } if (didBecomePrimary) { GeneralQueries.makePrimaryUser_Transaction(this, sqlCon, appIdentifier, userId); } @@ -3784,11 +3878,44 @@ public boolean linkAccounts_Transaction(AppIdentifier appIdentifier, Transaction return false; } + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // Use the LockedUser version of reserveAccountInfoForLinking_Transaction - boolean didLinkAccounts = AccountInfoQueries.reserveAccountInfoForLinking_Transaction( - this, sqlCon, appIdentifier, recipeUser, primaryUser); + boolean didLinkAccounts; + if (mode.writesToNewTables()) { + didLinkAccounts = AccountInfoQueries.reserveAccountInfoForLinking_Transaction( + this, sqlCon, appIdentifier, recipeUser, primaryUser); + } else { + // In LEGACY mode, check for conflicts using legacy tables + io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult checkResult = + checkIfLoginMethodsCanBeLinked_legacy(appIdentifier, primaryUserId, recipeUserId); + switch (checkResult.status) { + case OK: + didLinkAccounts = true; + break; + case WAS_ALREADY_LINKED_TO_PRIMARY_USER: + didLinkAccounts = false; + break; + case INPUT_USER_IS_NOT_PRIMARY_USER: + throw new InputUserIdIsNotAPrimaryUserException(primaryUserId); + case RECIPE_USER_LINKED_TO_ANOTHER_PRIMARY_USER: + // Need to get the recipe user info for the exception + AuthRecipeUserInfo recipeUserInfo = GeneralQueries.getPrimaryUserInfoForUserId( + this, appIdentifier, recipeUserId); + throw new CannotLinkSinceRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException( + recipeUserInfo); + case ACCOUNT_INFO_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER: + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + checkResult.conflictingPrimaryUserId, checkResult.message); + default: + throw new IllegalStateException("should never happen"); + } + } if (didLinkAccounts) { - GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId); + // Use the resolved primary user ID from LockedUserPair, not the raw parameter + // (the raw parameter might be a recipe user ID of an already-linked user) + String resolvedPrimaryUserId = primaryUser.getPrimaryUserId() != null + ? primaryUser.getPrimaryUserId() : primaryUserId; + GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, resolvedPrimaryUserId); } return didLinkAccounts; } catch (SQLException e) { @@ -3802,10 +3929,13 @@ public void unlinkAccounts_Transaction(AppIdentifier appIdentifier, TransactionC throws StorageQueryException { try { Connection sqlCon = (Connection) con.getConnection(); + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // 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); - AccountInfoQueries.doRemoveAccountInfoReservationForUnlinking(this, sqlCon, appIdentifier, recipeUserId); + if (mode.writesToNewTables()) { + AccountInfoQueries.doRemoveAccountInfoReservationForUnlinking(this, sqlCon, appIdentifier, recipeUserId); + } } catch (SQLException e) { throw new StorageQueryException(e); } @@ -3825,15 +3955,249 @@ public boolean doesUserIdExist_Transaction(TransactionConnection con, AppIdentif @Override public CanBecomePrimaryResult checkIfLoginMethodCanBecomePrimary(AppIdentifier appIdentifier, String recipeUserId) throws StorageQueryException, UnknownUserIdException { - return AccountInfoQueries.checkIfLoginMethodCanBecomePrimary(this, appIdentifier, recipeUserId); + if (Config.getConfig(this).getMigrationMode().readsFromNewTables()) { + return AccountInfoQueries.checkIfLoginMethodCanBecomePrimary(this, appIdentifier, recipeUserId); + } + return checkIfLoginMethodCanBecomePrimary_legacy(appIdentifier, recipeUserId); + } + + private void checkLegacyEmailConflict(AppIdentifier appIdentifier, String userId, String newEmail) + throws StorageQueryException, EmailChangeNotAllowedException { + try { + AuthRecipeUserInfo user = GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, userId); + if (user != null && user.isPrimaryUser) { + AuthRecipeUserInfo[] existingUsers = GeneralQueries.listPrimaryUsersByEmail_legacy_forApp(this, + appIdentifier, newEmail); + for (AuthRecipeUserInfo existingUser : existingUsers) { + if (existingUser.isPrimaryUser + && !existingUser.getSupertokensUserId().equals(user.getSupertokensUserId())) { + for (String tenantId : user.tenantIds) { + if (existingUser.tenantIds.contains(tenantId)) { + throw new EmailChangeNotAllowedException(); + } + } + } + } + } + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + private void checkLegacyPhoneConflict(AppIdentifier appIdentifier, String userId, String newPhone) + throws StorageQueryException, PhoneNumberChangeNotAllowedException { + try { + AuthRecipeUserInfo user = GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, userId); + if (user != null && user.isPrimaryUser) { + AuthRecipeUserInfo[] existingUsers = GeneralQueries.listPrimaryUsersByPhoneNumber_legacy_forApp(this, + appIdentifier, newPhone); + for (AuthRecipeUserInfo existingUser : existingUsers) { + if (existingUser.isPrimaryUser + && !existingUser.getSupertokensUserId().equals(user.getSupertokensUserId())) { + for (String tenantId : user.tenantIds) { + if (existingUser.tenantIds.contains(tenantId)) { + throw new PhoneNumberChangeNotAllowedException(); + } + } + } + } + } + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + private CanBecomePrimaryResult checkIfLoginMethodCanBecomePrimary_legacy(AppIdentifier appIdentifier, + String recipeUserId) + throws StorageQueryException, UnknownUserIdException { + try { + AuthRecipeUserInfo user = GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, recipeUserId); + if (user == null) { + throw new UnknownUserIdException(); + } + + if (user.isPrimaryUser) { + if (user.getSupertokensUserId().equals(recipeUserId)) { + return CanBecomePrimaryResult.wasAlreadyAPrimaryUserResult(); + } else { + return CanBecomePrimaryResult.linkedWithAnotherPrimaryUserResult(user.getSupertokensUserId()); + } + } + + // Check for conflicting account info with other primary users + for (LoginMethod lm : user.loginMethods) { + if (!lm.getSupertokensUserId().equals(recipeUserId)) continue; + + if (lm.email != null) { + AuthRecipeUserInfo[] usersWithEmail = GeneralQueries.listPrimaryUsersByEmail_legacy_forApp(this, + appIdentifier, lm.email); + for (AuthRecipeUserInfo u : usersWithEmail) { + if (u.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + if (u.tenantIds.contains(tenantId)) { + return CanBecomePrimaryResult.conflictingAccountInfoResult( + u.getSupertokensUserId(), + "This user's email is already associated with another user ID"); + } + } + } + } + } + + if (lm.phoneNumber != null) { + AuthRecipeUserInfo[] usersWithPhone = GeneralQueries.listPrimaryUsersByPhoneNumber_legacy_forApp( + this, appIdentifier, lm.phoneNumber); + for (AuthRecipeUserInfo u : usersWithPhone) { + if (u.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + if (u.tenantIds.contains(tenantId)) { + return CanBecomePrimaryResult.conflictingAccountInfoResult( + u.getSupertokensUserId(), + "This user's phone number is already associated with another user ID"); + } + } + } + } + } + + if (lm.thirdParty != null) { + List tpUserIds = ThirdPartyQueries.listPrimaryUserIdsByThirdPartyInfo_legacy(this, + appIdentifier, lm.thirdParty.id, lm.thirdParty.userId); + for (String tpUserId : tpUserIds) { + if (!tpUserId.equals(recipeUserId)) { + // Check tenant overlap before declaring conflict + AuthRecipeUserInfo tpUser = GeneralQueries.getPrimaryUserInfoForUserId(this, + appIdentifier, tpUserId); + if (tpUser != null && tpUser.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + if (tpUser.tenantIds.contains(tenantId)) { + return CanBecomePrimaryResult.conflictingAccountInfoResult(tpUserId, + "This user's third party login is already associated with another user ID"); + } + } + } + } + } + } + } + + return CanBecomePrimaryResult.okResult(); + } catch (SQLException e) { + throw new StorageQueryException(e); + } } @Override public io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult checkIfLoginMethodsCanBeLinked( AppIdentifier appIdentifier, String primaryUserId, String recipeUserId) throws StorageQueryException, UnknownUserIdException { - return AccountInfoQueries.checkIfLoginMethodsCanBeLinked(this, appIdentifier, - primaryUserId, recipeUserId); + if (Config.getConfig(this).getMigrationMode().readsFromNewTables()) { + return AccountInfoQueries.checkIfLoginMethodsCanBeLinked(this, appIdentifier, + primaryUserId, recipeUserId); + } + return checkIfLoginMethodsCanBeLinked_legacy(appIdentifier, primaryUserId, recipeUserId); + } + + private io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult checkIfLoginMethodsCanBeLinked_legacy( + AppIdentifier appIdentifier, String inputPrimaryUserId, String recipeUserId) + throws StorageQueryException, UnknownUserIdException { + try { + AuthRecipeUserInfo primaryUser = GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, + inputPrimaryUserId); + if (primaryUser == null) { + throw new UnknownUserIdException(); + } + if (!primaryUser.isPrimaryUser) { + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult.inputUserIsNotPrimaryUserResult(); + } + + AuthRecipeUserInfo recipeUser = GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, + recipeUserId); + if (recipeUser == null) { + throw new UnknownUserIdException(); + } + + if (recipeUser.isPrimaryUser) { + if (recipeUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult + .wasAlreadyLinkedToPrimaryUserResult(); + } else { + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult + .recipeUserLinkedToAnotherPrimaryUserResult(recipeUser.getSupertokensUserId()); + } + } + + // Check for conflicting account info — union of tenant IDs from both users + Set allTenantIds = new java.util.HashSet<>(); + for (String t : primaryUser.tenantIds) allTenantIds.add(t); + for (String t : recipeUser.tenantIds) allTenantIds.add(t); + + // Check all login methods of both users for conflicts + List allLoginMethods = new java.util.ArrayList<>(); + allLoginMethods.addAll(java.util.Arrays.asList(primaryUser.loginMethods)); + allLoginMethods.addAll(java.util.Arrays.asList(recipeUser.loginMethods)); + + for (LoginMethod lm : allLoginMethods) { + if (lm.email != null) { + AuthRecipeUserInfo[] usersWithEmail = GeneralQueries.listPrimaryUsersByEmail_legacy_forApp(this, + appIdentifier, lm.email); + for (AuthRecipeUserInfo u : usersWithEmail) { + if (u.isPrimaryUser && !u.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + for (String tenantId : allTenantIds) { + if (u.tenantIds.contains(tenantId)) { + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult + .notOkResult(u.getSupertokensUserId(), + "This user's email is already associated with another user ID"); + } + } + } + } + } + + if (lm.phoneNumber != null) { + AuthRecipeUserInfo[] usersWithPhone = GeneralQueries.listPrimaryUsersByPhoneNumber_legacy_forApp( + this, appIdentifier, lm.phoneNumber); + for (AuthRecipeUserInfo u : usersWithPhone) { + if (u.isPrimaryUser && !u.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + for (String tenantId : allTenantIds) { + if (u.tenantIds.contains(tenantId)) { + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult + .notOkResult(u.getSupertokensUserId(), + "This user's phone number is already associated with another user ID"); + } + } + } + } + } + + if (lm.thirdParty != null) { + List tpUserIds = ThirdPartyQueries.listPrimaryUserIdsByThirdPartyInfo_legacy(this, + appIdentifier, lm.thirdParty.id, lm.thirdParty.userId); + for (String tpUserId : tpUserIds) { + // Exclude the primary user we're linking to AND the recipe user being linked + if (!tpUserId.equals(primaryUser.getSupertokensUserId()) + && !tpUserId.equals(recipeUserId)) { + // Also check tenant overlap + AuthRecipeUserInfo tpUser = GeneralQueries.getPrimaryUserInfoForUserId(this, + appIdentifier, tpUserId); + if (tpUser != null && tpUser.isPrimaryUser) { + for (String tenantId : allTenantIds) { + if (tpUser.tenantIds.contains(tenantId)) { + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult + .notOkResult(tpUserId, + "This user's third party login is already associated with another user ID"); + } + } + } + } + } + } + } + + return io.supertokens.pluginInterface.authRecipe.CanLinkAccountsResult.okResult(); + } catch (SQLException e) { + throw new StorageQueryException(e); + } } @Override @@ -3843,18 +4207,171 @@ public void addTenantIdToPrimaryUser_Transaction(TenantIdentifier tenantIdentifi AnotherPrimaryUserWithEmailAlreadyExistsException, AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException, StorageQueryException { - AccountInfoQueries.addTenantIdToPrimaryUser_Transaction(this, con, tenantIdentifier, primaryUser); + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (mode.writesToNewTables()) { + AccountInfoQueries.addTenantIdToPrimaryUser_Transaction(this, con, tenantIdentifier, primaryUser); + } else { + // In LEGACY mode, check for email/phone/thirdparty conflicts with other primary users on this tenant + try { + AppIdentifier appIdentifier = tenantIdentifier.toAppIdentifier(); + String primaryUserId = primaryUser.getPrimaryUserId() != null ? primaryUser.getPrimaryUserId() : primaryUser.getRecipeUserId(); + AuthRecipeUserInfo user = GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, primaryUserId); + if (user != null && user.isPrimaryUser) { + for (LoginMethod lm : user.loginMethods) { + if (lm.email != null) { + AuthRecipeUserInfo[] usersWithEmail = GeneralQueries.listPrimaryUsersByEmail_legacy_forApp(this, + appIdentifier, lm.email); + for (AuthRecipeUserInfo u : usersWithEmail) { + if (u.isPrimaryUser && !u.getSupertokensUserId().equals(primaryUserId) + && u.tenantIds.contains(tenantIdentifier.getTenantId())) { + throw new AnotherPrimaryUserWithEmailAlreadyExistsException(u.getSupertokensUserId()); + } + } + } + if (lm.phoneNumber != null) { + AuthRecipeUserInfo[] usersWithPhone = GeneralQueries.listPrimaryUsersByPhoneNumber_legacy_forApp( + this, appIdentifier, lm.phoneNumber); + for (AuthRecipeUserInfo u : usersWithPhone) { + if (u.isPrimaryUser && !u.getSupertokensUserId().equals(primaryUserId) + && u.tenantIds.contains(tenantIdentifier.getTenantId())) { + throw new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(u.getSupertokensUserId()); + } + } + } + if (lm.thirdParty != null) { + List tpUserIds = ThirdPartyQueries.listPrimaryUserIdsByThirdPartyInfo_legacy(this, + appIdentifier, lm.thirdParty.id, lm.thirdParty.userId); + for (String tpUserId : tpUserIds) { + if (!tpUserId.equals(primaryUserId)) { + throw new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(tpUserId); + } + } + } + } + } + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } @Override public void deleteAccountInfoReservations_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { - AccountInfoQueries.removeAccountInfoReservationsForDeletingUser_Transaction(this, con, appIdentifier, userId); + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (mode.writesToNewTables()) { + AccountInfoQueries.removeAccountInfoReservationsForDeletingUser_Transaction(this, con, appIdentifier, userId); + } } @Override public void reservePrimaryUserAccountInfos_Transaction(TransactionConnection con, List primaryUsers) throws StorageQueryException, StorageTransactionLogicException { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (!mode.writesToNewTables()) { + // In LEGACY mode, check for conflicts using old tables + // First, check for intra-batch conflicts (two PrimaryUsers in the same batch + // with the same account info on overlapping tenants) + Map intraBatchErrors = new HashMap<>(); + for (int i = 0; i < primaryUsers.size(); i++) { + PrimaryUser pu1 = primaryUsers.get(i); + for (int j = i + 1; j < primaryUsers.size(); j++) { + PrimaryUser pu2 = primaryUsers.get(j); + if (pu1.primaryUserId.equals(pu2.primaryUserId)) { + continue; + } + // Check if they share any tenant + boolean sharesTenant = false; + for (String t : pu1.tenantIds) { + if (pu2.tenantIds.contains(t)) { + sharesTenant = true; + break; + } + } + if (!sharesTenant) { + continue; + } + // Check if they share any account info + for (PrimaryUser.AccountInfo ai1 : pu1.accountInfos) { + for (PrimaryUser.AccountInfo ai2 : pu2.accountInfos) { + if (ai1.type == ai2.type && ai1.value.equals(ai2.value)) { + // pu2 conflicts with pu1 — mark pu2 as error + intraBatchErrors.put(pu2.primaryUserId, + new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + pu1.primaryUserId, + "E027: there is a conflicting account info")); + } + } + } + } + } + if (!intraBatchErrors.isEmpty()) { + throw new StorageTransactionLogicException( + new BulkImportBatchInsertException("conflict", intraBatchErrors)); + } + + try { + for (PrimaryUser pu : primaryUsers) { + for (PrimaryUser.AccountInfo ai : pu.accountInfos) { + for (String tenantId : pu.tenantIds) { + if (ai.type == ACCOUNT_INFO_TYPE.EMAIL) { + AuthRecipeUserInfo[] users = GeneralQueries.listPrimaryUsersByEmail_legacy_forApp(this, + pu.appIdentifier, ai.value); + for (AuthRecipeUserInfo u : users) { + if (u.isPrimaryUser && !u.getSupertokensUserId().equals(pu.primaryUserId) + && u.tenantIds.contains(tenantId)) { + throw new StorageTransactionLogicException( + new BulkImportBatchInsertException("conflict", + java.util.Map.of(pu.primaryUserId, + new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + u.getSupertokensUserId(), + "E027: there is a conflicting account info")))); + } + } + } else if (ai.type == ACCOUNT_INFO_TYPE.PHONE_NUMBER) { + AuthRecipeUserInfo[] users = GeneralQueries.listPrimaryUsersByPhoneNumber_legacy_forApp(this, + pu.appIdentifier, ai.value); + for (AuthRecipeUserInfo u : users) { + if (u.isPrimaryUser && !u.getSupertokensUserId().equals(pu.primaryUserId) + && u.tenantIds.contains(tenantId)) { + throw new StorageTransactionLogicException( + new BulkImportBatchInsertException("conflict", + java.util.Map.of(pu.primaryUserId, + new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + u.getSupertokensUserId(), + "E027: there is a conflicting account info")))); + } + } + } else if (ai.type == ACCOUNT_INFO_TYPE.THIRD_PARTY) { + // Third party account info value is "thirdPartyId::thirdPartyUserId" + String[] parts = ai.value.split("::", 2); + if (parts.length == 2) { + List tpUserIds = ThirdPartyQueries.listPrimaryUserIdsByThirdPartyInfo_legacy( + this, pu.appIdentifier, parts[0], parts[1]); + for (String tpUserId : tpUserIds) { + if (!tpUserId.equals(pu.primaryUserId)) { + AuthRecipeUserInfo tpUser = GeneralQueries.getPrimaryUserInfoForUserId(this, + pu.appIdentifier, tpUserId); + if (tpUser != null && tpUser.isPrimaryUser && tpUser.tenantIds.contains(tenantId)) { + throw new StorageTransactionLogicException( + new BulkImportBatchInsertException("conflict", + java.util.Map.of(pu.primaryUserId, + new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + tpUserId, + "E027: there is a conflicting account info")))); + } + } + } + } + } + } + } + } + } catch (SQLException e) { + throw new StorageQueryException(e); + } + return; + } try { AccountInfoQueries.reservePrimaryUserAccountInfos_Transaction(this, con, primaryUsers); } catch (SQLException e) { @@ -4679,9 +5196,15 @@ public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, Trans DuplicateEmailException, EmailChangeNotAllowedException, UnknownUserIdException { try { Connection sqlCon = (Connection) con.getConnection(); + MigrationMode mode = Config.getConfig(this).getMigrationMode(); // Acquire lock to get LockedUser for the new API LockedUser lockedUser = UserLockingQueries.lockUser(this, sqlCon, tenantIdentifier.toAppIdentifier(), userId); - AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, tenantIdentifier.toAppIdentifier(), lockedUser, ACCOUNT_INFO_TYPE.EMAIL, newEmail); + if (mode.writesToNewTables()) { + AccountInfoQueries.updateAccountInfo_Transaction(this, sqlCon, tenantIdentifier.toAppIdentifier(), lockedUser, ACCOUNT_INFO_TYPE.EMAIL, newEmail); + } else { + // Legacy email conflict check + checkLegacyEmailConflict(tenantIdentifier.toAppIdentifier(), userId, newEmail); + } WebAuthNQueries.updateUserEmail_Transaction(this, sqlCon, tenantIdentifier, userId, newEmail); } catch (StorageQueryException e) { if (e.getCause() instanceof SQLException){ @@ -4911,6 +5434,10 @@ public boolean reserveAccountInfoForLinking_Transaction( InputUserIdIsNotAPrimaryUserException, CannotLinkSinceRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (!mode.writesToNewTables()) { + return true; + } Connection sqlCon = (Connection) con.getConnection(); return AccountInfoQueries.reserveAccountInfoForLinking_Transaction( this, sqlCon, appIdentifier, recipeUser, primaryUser); @@ -4926,6 +5453,10 @@ public void updateAccountInfo_Transaction( throws StorageQueryException, UnknownUserIdException, EmailChangeNotAllowedException, PhoneNumberChangeNotAllowedException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateThirdPartyUserException { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (!mode.writesToNewTables()) { + return; + } Connection sqlCon = (Connection) con.getConnection(); AccountInfoQueries.updateAccountInfo_Transaction( this, sqlCon, appIdentifier, user, accountInfoType, newAccountInfoValue); @@ -4939,6 +5470,10 @@ public boolean addPrimaryUserAccountInfo_Transaction( throws StorageQueryException, UnknownUserIdException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, CannotBecomePrimarySinceRecipeUserIdAlreadyLinkedWithPrimaryUserIdException { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (!mode.writesToNewTables()) { + return true; + } Connection sqlCon = (Connection) con.getConnection(); return AccountInfoQueries.addPrimaryUserAccountInfo_Transaction( this, sqlCon, appIdentifier, primaryUser); @@ -4950,6 +5485,10 @@ public void removeAccountInfoReservationForPrimaryUserForUnlinking_Transaction( TransactionConnection con, LockedUser recipeUser) throws StorageQueryException { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (!mode.writesToNewTables()) { + return; + } Connection sqlCon = (Connection) con.getConnection(); AccountInfoQueries.removeAccountInfoReservationForPrimaryUserForUnlinking_Transaction( this, sqlCon, appIdentifier, recipeUser); @@ -4962,6 +5501,10 @@ public void addTenantIdToRecipeUser_Transaction( LockedUser user) throws StorageQueryException, DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { + MigrationMode mode = Config.getConfig(this).getMigrationMode(); + if (!mode.writesToNewTables()) { + return; + } Connection sqlCon = (Connection) con.getConnection(); AccountInfoQueries.addTenantIdToRecipeUser_Transaction( this, sqlCon, tenantIdentifier, user); diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index 36f2c076..a44b64b2 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -25,6 +25,7 @@ import com.google.gson.JsonObject; import io.supertokens.pluginInterface.ConfigFieldInfo; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.annotations.*; @@ -199,6 +200,15 @@ public class PostgreSQLConfig { private Integer postgresql_minimum_idle_connections = null; + @EnvName("SUPERTOKENS_MIGRATION_MODE") + @JsonProperty + @IgnoreForAnnotationCheck + @DashboardInfo( + description = "Migration mode for all_auth_recipe_users table deprecation. " + + "Values: LEGACY, DUAL_WRITE_READ_OLD, DUAL_WRITE_READ_NEW, MIGRATED", + defaultValue = "\"LEGACY\"", isOptional = true) + private String migration_mode = null; + @IgnoreForAnnotationCheck boolean isValidAndNormalised = false; @@ -319,6 +329,17 @@ public String getConnectionURI() { return postgresql_connection_uri; } + public MigrationMode getMigrationMode() { + if (migration_mode == null) { + return MigrationMode.LEGACY; + } + try { + return MigrationMode.valueOf(migration_mode.toUpperCase()); + } catch (IllegalArgumentException e) { + return MigrationMode.LEGACY; + } + } + public String getUsersTable() { return addSchemaAndPrefixToTableName("all_auth_recipe_users"); } @@ -576,6 +597,16 @@ private void validateAndNormalise(boolean skipValidation) throws InvalidConfigEx + "'postgresql_connection_pool_size'"); } } + + if (migration_mode != null) { + try { + MigrationMode.valueOf(migration_mode.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new InvalidConfigException( + "Invalid migration_mode value: '" + migration_mode + + "'. Must be one of: LEGACY, DUAL_WRITE_READ_OLD, DUAL_WRITE_READ_NEW, MIGRATED"); + } + } } // Normalisation 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 10b7479b..85e91867 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -30,6 +30,7 @@ import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.EMAIL_PASSWORD; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.ACCOUNT_INFO_TYPE; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; @@ -156,7 +157,9 @@ public static void updateUsersPassword_Transaction(Start start, Connection con, public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newEmail) throws SQLException { - { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + { // emailpassword_users — ALWAYS String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable() + " SET email = ? WHERE app_id = ? AND user_id = ?"; @@ -166,7 +169,7 @@ public static void updateUsersEmail_Transaction(Start start, Connection con, App pst.setString(3, userId); }); } - { + if (mode.writesToOldTables()) { // emailpassword_user_to_tenant String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUserToTenantTable() + " SET email = ? WHERE app_id = ? AND user_id = ?"; @@ -289,7 +292,9 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - { // app_id_to_user_id + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + { // app_id_to_user_id — ALWAYS String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?)"; @@ -303,7 +308,7 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } - { // all_auth_recipe_users + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + @@ -320,12 +325,12 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } - { // recipe_user_tenants + if (mode.writesToNewTables()) { // recipe_user_tenants AccountInfoQueries.addRecipeUserAccountInfo_Transaction(start, sqlCon, tenantIdentifier, userId, EMAIL_PASSWORD.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", email); } - { // emailpassword_users + { // emailpassword_users — ALWAYS String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUsersTable() + "(app_id, user_id, email, password_hash, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; @@ -338,7 +343,7 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } - { // emailpassword_user_to_tenant + if (mode.writesToOldTables()) { // emailpassword_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + "(app_id, tenant_id, user_id, email)" + " VALUES(?, ?, ?, ?)"; @@ -364,6 +369,8 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden public static void importUsers_Transaction(Start start, Connection sqlCon, List usersToSignUp) throws StorageQueryException, StorageTransactionLogicException { try { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + String app_id_to_user_id_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + "(app_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(?, ?, ?, ?, ?, ?, ?)"; @@ -395,11 +402,12 @@ public static void importUsers_Transaction(Start start, Connection sqlCon, List< boolean isLinkedOrIsPrimaryUser = user.primaryUserId != null; // Recipe Account Info - AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, EMAIL_PASSWORD.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); - - // Recipe User Tenants - AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, EMAIL_PASSWORD.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, user.recipeUserTenantIds); + if (mode.writesToNewTables()) { + AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, EMAIL_PASSWORD.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); + // Recipe User Tenants + AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, EMAIL_PASSWORD.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, user.recipeUserTenantIds); + } appIdToUserIdSetters.add(pst -> { pst.setString(1, appId); @@ -421,33 +429,41 @@ public static void importUsers_Transaction(Start start, Connection sqlCon, List< // Generate entries for all recipe user tenant IDs for (String tenantId : user.recipeUserTenantIds) { - allAuthRecipeUsersSetters.add(pst -> { - pst.setString(1, appId); - pst.setString(2, tenantId); - pst.setString(3, userId); - pst.setString(4, primaryOrRecipeUserId); - pst.setBoolean(5, isLinkedOrIsPrimaryUser); - pst.setString(6, EMAIL_PASSWORD.toString()); - pst.setLong(7, user.timeJoinedMSSinceEpoch); - pst.setLong(8, user.timeJoinedMSSinceEpoch); - }); - - emailPasswordUsersToTenantSetters.add(pst -> { - pst.setString(1, appId); - pst.setString(2, tenantId); - pst.setString(3, userId); - pst.setString(4, user.email); - }); + if (mode.writesToOldTables()) { + allAuthRecipeUsersSetters.add(pst -> { + pst.setString(1, appId); + pst.setString(2, tenantId); + pst.setString(3, userId); + pst.setString(4, primaryOrRecipeUserId); + pst.setBoolean(5, isLinkedOrIsPrimaryUser); + pst.setString(6, EMAIL_PASSWORD.toString()); + pst.setLong(7, user.timeJoinedMSSinceEpoch); + pst.setLong(8, user.timeJoinedMSSinceEpoch); + }); + + emailPasswordUsersToTenantSetters.add(pst -> { + pst.setString(1, appId); + pst.setString(2, tenantId); + pst.setString(3, userId); + pst.setString(4, user.email); + }); + } } } - executeBatch(sqlCon, AccountInfoQueries.getRecipeUserAccountInfoBatchQuery(start), recipeUserAccountInfoBatch); - executeBatch(sqlCon, AccountInfoQueries.getRecipeUserTenantBatchQuery(start), recipeUserTenantsBatch); + if (mode.writesToNewTables()) { + executeBatch(sqlCon, AccountInfoQueries.getRecipeUserAccountInfoBatchQuery(start), recipeUserAccountInfoBatch); + executeBatch(sqlCon, AccountInfoQueries.getRecipeUserTenantBatchQuery(start), recipeUserTenantsBatch); + } executeBatch(sqlCon, app_id_to_user_id_QUERY, appIdToUserIdSetters); - executeBatch(sqlCon, all_auth_recipe_users_QUERY, allAuthRecipeUsersSetters); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, all_auth_recipe_users_QUERY, allAuthRecipeUsersSetters); + } executeBatch(sqlCon, emailpassword_users_QUERY, emailPasswordUsersSetters); - executeBatch(sqlCon, emailpassword_users_to_tenant_QUERY, emailPasswordUsersToTenantSetters); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, emailpassword_users_to_tenant_QUERY, emailPasswordUsersToTenantSetters); + } sqlCon.commit(); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -457,7 +473,10 @@ public static void importUsers_Transaction(Start start, Connection sqlCon, List< public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, String userId, boolean deleteUserIdMappingToo) throws SQLException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + if (deleteUserIdMappingToo) { + // Deleting from app_id_to_user_id cascades to all child tables String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; @@ -466,7 +485,7 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde pst.setString(2, userId); }); } else { - { + if (mode.writesToOldTables()) { String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { @@ -475,7 +494,7 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde }); } - { + { // emailpassword_users — ALWAYS String QUERY = "DELETE FROM " + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { @@ -484,7 +503,7 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde }); } - { + { // password_reset_tokens — ALWAYS String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { @@ -582,6 +601,36 @@ public static List getUsersInfoUsingIdList_Transaction(Start start, public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserIdUsingEmail_new(start, tenantIdentifier, email); + } + return getPrimaryUserIdUsingEmail_legacy(start, tenantIdentifier, email); + } + + private static String getPrimaryUserIdUsingEmail_legacy(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 = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + } + + private static String getPrimaryUserIdUsingEmail_new(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserTenantsTable() + " AS rut" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS auid" @@ -604,6 +653,8 @@ public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier te public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException, UnknownUserIdException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); @@ -614,7 +665,7 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); - { // all_auth_recipe_users + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, " + @@ -637,7 +688,7 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC finalAccountLinkingInfo.primaryUserId); } - { // emailpassword_user_to_tenant + if (mode.writesToOldTables()) { // emailpassword_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + "(app_id, tenant_id, user_id, email)" + " VALUES(?, ?, ?, ?) " + " ON CONFLICT ON CONSTRAINT " @@ -654,12 +705,16 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC return numRows > 0; } + + return true; } public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - { // all_auth_recipe_users + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND tenant_id = ? and user_id = ? and recipe_id = ?"; int numRows = update(sqlCon, QUERY, pst -> { @@ -669,8 +724,10 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection pst.setString(4, EMAIL_PASSWORD.toString()); }); return numRows > 0; + // automatically deleted from emailpassword_user_to_tenant because of foreign key constraint } - // automatically deleted from emailpassword_user_to_tenant because of foreign key constraint + + return true; } private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, 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 c67bc983..8f95c97e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -33,6 +33,7 @@ import org.jetbrains.annotations.TestOnly; import io.supertokens.pluginInterface.KeyValueInfo; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; @@ -970,6 +971,14 @@ public static void deleteKeyValue_Transaction(Start start, Connection con, Tenan public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getUsersCount_new(start, appIdentifier, includeRecipeIds); + } + return getUsersCount_legacy(start, appIdentifier, includeRecipeIds); + } + + private static long getUsersCount_new(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) + throws SQLException, StorageQueryException { StringBuilder QUERY = new StringBuilder( "SELECT COUNT(*) AS total FROM ("); QUERY.append("SELECT primary_or_recipe_user_id FROM " + getConfig(start).getAppIdToUserIdTable()); @@ -1003,8 +1012,51 @@ public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIP }); } + private static long getUsersCount_legacy(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) + throws SQLException, StorageQueryException { + StringBuilder QUERY = new StringBuilder( + "SELECT COUNT(*) AS total FROM ("); + QUERY.append("SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable()); + QUERY.append(" WHERE app_id = ?"); + if (includeRecipeIds != null && includeRecipeIds.length > 0) { + QUERY.append(" AND recipe_id IN ("); + for (int i = 0; i < includeRecipeIds.length; i++) { + QUERY.append("?"); + if (i != includeRecipeIds.length - 1) { + // not the last element + QUERY.append(","); + } + } + QUERY.append(")"); + } + QUERY.append(" GROUP BY primary_or_recipe_user_id) AS uniq_users"); + + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + if (includeRecipeIds != null) { + for (int i = 0; i < includeRecipeIds.length; i++) { + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, includeRecipeIds[i].toString()); + } + } + }, result -> { + if (result.next()) { + return result.getLong("total"); + } + return 0L; + }); + } + public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getUsersCount_new(start, tenantIdentifier, includeRecipeIds); + } + return getUsersCount_legacy(start, tenantIdentifier, includeRecipeIds); + } + + private static long getUsersCount_new(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) + throws SQLException, StorageQueryException { StringBuilder QUERY = new StringBuilder( "SELECT COUNT(*) AS total FROM ("); QUERY.append("SELECT auid.primary_or_recipe_user_id FROM " + getConfig(start).getRecipeUserTenantsTable() + " rut"); @@ -1041,6 +1093,43 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, }); } + private static long getUsersCount_legacy(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) + throws SQLException, StorageQueryException { + StringBuilder QUERY = new StringBuilder( + "SELECT COUNT(*) AS total FROM ("); + QUERY.append("SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable()); + QUERY.append(" WHERE app_id = ? AND tenant_id = ?"); + if (includeRecipeIds != null && includeRecipeIds.length > 0) { + QUERY.append(" AND recipe_id IN ("); + for (int i = 0; i < includeRecipeIds.length; i++) { + QUERY.append("?"); + if (i != includeRecipeIds.length - 1) { + // not the last element + QUERY.append(","); + } + } + QUERY.append(")"); + } + + QUERY.append(" GROUP BY primary_or_recipe_user_id) AS uniq_users"); + + return execute(start, QUERY.toString(), pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + if (includeRecipeIds != null) { + for (int i = 0; i < includeRecipeIds.length; i++) { + // i+3 cause this starts with 1 and not 0, and 1 is appId, 2 is tenantId + pst.setString(i + 3, includeRecipeIds[i].toString()); + } + } + }, result -> { + if (result.next()) { + return result.getLong("total"); + } + return 0L; + }); + } + 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 @@ -1068,6 +1157,14 @@ public static boolean doesUserIdExist_Transaction(Start start, Connection sqlCon public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return doesUserIdExist_new(start, tenantIdentifier, userId); + } + return doesUserIdExist_legacy(start, tenantIdentifier, userId); + } + + private static boolean doesUserIdExist_new(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).getRecipeUserTenantsTable() @@ -1084,6 +1181,24 @@ public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdenti }, ResultSet::next); } + private static boolean doesUserIdExist_legacy(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 = ? UNION SELECT 1 FROM " + + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? AND primary_or_recipe_user_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, tenantIdentifier.getAppId()); + pst.setString(5, tenantIdentifier.getTenantId()); + pst.setString(6, userId); + }, ResultSet::next); + } + public static List findUserIdsThatExist(Start start, AppIdentifier appIdentifier, List userIds) throws SQLException, StorageQueryException { if (userIds == null || userIds.isEmpty()){ @@ -1111,6 +1226,21 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getUsers_new(start, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined, + dashboardSearchTags); + } + return getUsers_legacy(start, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, timeJoined, + dashboardSearchTags); + } + + private static AuthRecipeUserInfo[] getUsers_new(Start start, TenantIdentifier tenantIdentifier, + @NotNull Integer limit, + @NotNull String timeJoinedOrder, + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, + @Nullable Long timeJoined, + @Nullable DashboardSearchTags dashboardSearchTags) + throws SQLException, StorageQueryException { // This list will be used to keep track of the result's order from the db List usersFromQuery; @@ -1356,10 +1486,317 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant return finalResult; } + private static AuthRecipeUserInfo[] getUsers_legacy(Start start, TenantIdentifier tenantIdentifier, + @NotNull Integer limit, + @NotNull String timeJoinedOrder, + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, + @Nullable Long timeJoined, + @Nullable DashboardSearchTags dashboardSearchTags) + throws SQLException, StorageQueryException { + + // This list will be used to keep track of the result's order from the db + List usersFromQuery; + + if (dashboardSearchTags != null) { + ArrayList queryList = new ArrayList<>(); + { + StringBuilder USER_SEARCH_TAG_CONDITION = new StringBuilder(); + + { + // check if we should search through the emailpassword table + if (dashboardSearchTags.shouldEmailPasswordTableBeSearched()) { + String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + + " 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 + QUERY = QUERY + + " WHERE (emailpasswordTable.app_id = ? AND emailpasswordTable.tenant_id = ?) AND" + + " ( emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ? "; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); + queryList.add(dashboardSearchTags.emails.get(0) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { + QUERY += " OR emailpasswordTable.email LIKE ? OR emailpasswordTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(i) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%"); + } + + QUERY += " )"; + + USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS emailpasswordResultTable"); + } + } + + { + // 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).getThirdPartyUserToTenantTable() + + + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable" + + ".app_id AND" + + " allAuthUsersTable.tenant_id = thirdPartyToTenantTable.tenant_id AND" + + " allAuthUsersTable.user_id = thirdPartyToTenantTable.user_id" + + " JOIN " + getConfig(start).getThirdPartyUsersTable() + + " 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) { + + QUERY += + " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable.tenant_id" + + " = ?)" + + " AND ( thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); + queryList.add(dashboardSearchTags.emails.get(0) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); + + for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { + QUERY += " OR thirdPartyTable.email LIKE ? OR thirdPartyTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(i) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%"); + } + + QUERY += " )"; + + } + + // check if providers tag is present + if (dashboardSearchTags.providers != null) { + if (dashboardSearchTags.emails != null) { + QUERY += " AND "; + } else { + QUERY += " WHERE (thirdPartyToTenantTable.app_id = ? AND thirdPartyToTenantTable" + + ".tenant_id = ?) AND "; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); + } + + QUERY += " ( thirdPartyTable.third_party_id LIKE ?"; + queryList.add(dashboardSearchTags.providers.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.providers.size(); i++) { + QUERY += " OR thirdPartyTable.third_party_id LIKE ?"; + queryList.add(dashboardSearchTags.providers.get(i) + "%"); + } + + QUERY += " )"; + } + + // check if we need to append this to an existing search query + if (USER_SEARCH_TAG_CONDITION.length() != 0) { + USER_SEARCH_TAG_CONDITION.append(" UNION ").append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS thirdPartyResultTable"); + + } else { + USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS thirdPartyResultTable"); + + } + } + } + + { + // check if we should search through the passwordless table + if (dashboardSearchTags.shouldPasswordlessTableBeSearched()) { + String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + + " 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 + if (dashboardSearchTags.emails != null) { + + QUERY = QUERY + " WHERE (passwordlessTable.app_id = ? AND passwordlessTable.tenant_id = ?)" + + " AND ( passwordlessTable.email LIKE ? OR passwordlessTable.email LIKE ?"; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); + queryList.add(dashboardSearchTags.emails.get(0) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.emails.size(); i++) { + QUERY += " OR passwordlessTable.email LIKE ? OR passwordlessTable.email LIKE ?"; + queryList.add(dashboardSearchTags.emails.get(i) + "%"); + queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%"); + } + + QUERY += " )"; + } + + // check if phone tag is present + if (dashboardSearchTags.phoneNumbers != null) { + + if (dashboardSearchTags.emails != null) { + QUERY += " AND "; + } else { + QUERY += " WHERE (passwordlessTable.app_id = ? AND passwordlessTable.tenant_id = ?) " + + "AND "; + queryList.add(tenantIdentifier.getAppId()); + queryList.add(tenantIdentifier.getTenantId()); + } + + QUERY += " ( passwordlessTable.phone_number LIKE ?"; + queryList.add(dashboardSearchTags.phoneNumbers.get(0) + "%"); + for (int i = 1; i < dashboardSearchTags.phoneNumbers.size(); i++) { + QUERY += " OR passwordlessTable.phone_number LIKE ?"; + queryList.add(dashboardSearchTags.phoneNumbers.get(i) + "%"); + } + + QUERY += " )"; + } + + // check if we need to append this to an existing search query + if (USER_SEARCH_TAG_CONDITION.length() != 0) { + USER_SEARCH_TAG_CONDITION.append(" UNION ").append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS passwordlessResultTable"); + + } else { + USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY) + .append(" LIMIT 1000) AS passwordlessResultTable"); + + } + } + } + + if (USER_SEARCH_TAG_CONDITION.toString().length() == 0) { + usersFromQuery = new ArrayList<>(); + } else { + + 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<>(); + while (result.next()) { + temp.add(result.getString("primary_or_recipe_user_id")); + } + return temp; + }); + } + } + + } else { + StringBuilder RECIPE_ID_CONDITION = new StringBuilder(); + if (includeRecipeIds != null && includeRecipeIds.length > 0) { + RECIPE_ID_CONDITION.append("recipe_id IN ("); + for (int i = 0; i < includeRecipeIds.length; i++) { + + RECIPE_ID_CONDITION.append("?"); + if (i != includeRecipeIds.length - 1) { + // not the last element + RECIPE_ID_CONDITION.append(","); + } + } + RECIPE_ID_CONDITION.append(")"); + } + + if (timeJoined != null && userId != null) { + String recipeIdCondition = RECIPE_ID_CONDITION.toString(); + if (!recipeIdCondition.equals("")) { + recipeIdCondition = recipeIdCondition + " AND"; + } + String timeJoinedOrderSymbol = timeJoinedOrder.equals("ASC") ? ">" : "<"; + 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++) { + // i+1 cause this starts with 1 and not 0 + pst.setString(i + 1, includeRecipeIds[i].toString()); + } + } + int baseIndex = includeRecipeIds == null ? 0 : includeRecipeIds.length; + pst.setLong(baseIndex + 1, timeJoined); + pst.setLong(baseIndex + 2, timeJoined); + pst.setString(baseIndex + 3, userId); + pst.setString(baseIndex + 4, tenantIdentifier.getAppId()); + pst.setString(baseIndex + 5, tenantIdentifier.getTenantId()); + pst.setInt(baseIndex + 6, limit); + }, result -> { + List temp = new ArrayList<>(); + while (result.next()) { + temp.add(result.getString("primary_or_recipe_user_id")); + } + return temp; + }); + } else { + String recipeIdCondition = RECIPE_ID_CONDITION.toString(); + 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 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++) { + // i+1 cause this starts with 1 and not 0 + pst.setString(i + 1, includeRecipeIds[i].toString()); + } + } + int baseIndex = includeRecipeIds == null ? 0 : includeRecipeIds.length; + pst.setString(baseIndex + 1, tenantIdentifier.getAppId()); + pst.setString(baseIndex + 2, tenantIdentifier.getTenantId()); + pst.setInt(baseIndex + 3, limit); + }, result -> { + List temp = new ArrayList<>(); + while (result.next()) { + temp.add(result.getString("primary_or_recipe_user_id")); + } + return temp; + }); + } + } + + 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)); + } + } + + return finalResult; + } + public static void makePrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { String QUERY = "UPDATE " + getConfig(start).getUsersTable() + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; @@ -1382,6 +1819,7 @@ public static void makePrimaryUser_Transaction(Start start, Connection sqlCon, A public static void makePrimaryUsers_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userIds) throws SQLException, StorageQueryException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); String users_update_QUERY = "UPDATE " + getConfig(start).getUsersTable() + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; @@ -1402,41 +1840,73 @@ public static void makePrimaryUsers_Transaction(Start start, Connection sqlCon, }); } - executeBatch(sqlCon, users_update_QUERY, usersUpdateBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, users_update_QUERY, usersUpdateBatch); + } executeBatch(sqlCon, appid_to_userid_update_QUERY, appIdToUserIdUpdateBatch); } 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 = (" + - " SELECT primary_user_id FROM " + getConfig(start).getRecipeUserAccountInfosTable() + - " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" + - ") WHERE app_id = ? AND user_id = ?"; - - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, primaryUserId); - pst.setString(3, appIdentifier.getAppId()); - pst.setString(4, recipeUserId); - }); + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { + if (mode.writesToNewTables()) { + // DUAL_WRITE mode: get primary_user_id from reservation tables + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = (" + + " SELECT primary_user_id FROM " + getConfig(start).getRecipeUserAccountInfosTable() + + " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" + + ") WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, primaryUserId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, recipeUserId); + }); + } else { + // LEGACY mode: use primaryUserId directly (reservation tables are empty) + 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); + }); + } } { - String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + - " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = (" + - " SELECT primary_user_id FROM " + getConfig(start).getRecipeUserAccountInfosTable() + - " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" + - ") WHERE app_id = ? AND user_id = ?"; - - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, primaryUserId); - pst.setString(3, appIdentifier.getAppId()); - pst.setString(4, recipeUserId); - }); + if (mode.writesToNewTables()) { + // DUAL_WRITE or MIGRATED: get primary_user_id from reservation tables + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = (" + + " SELECT primary_user_id FROM " + getConfig(start).getRecipeUserAccountInfosTable() + + " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" + + ") WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, primaryUserId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, recipeUserId); + }); + } else { + // LEGACY mode: use primaryUserId directly + 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); + }); + } } // Must be called AFTER both all_auth_recipe_users and app_id_to_user_id have been updated @@ -1452,6 +1922,8 @@ public static void linkMultipleAccounts_Transaction(Start start, Connection sqlC return; } + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + String update_users_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 = ?"; @@ -1478,7 +1950,9 @@ public static void linkMultipleAccounts_Transaction(Start start, Connection sqlC pst.setString(3, recipeUserId); }); } - executeBatch(sqlCon, update_users_QUERY, usersUpdateBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, update_users_QUERY, usersUpdateBatch); + } executeBatch(sqlCon, update_appid_to_userid_QUERY, appIdToUserIdUpdateBatch); updateTimeJoinedForPrimaryUsers_Transaction(start, sqlCon, appIdentifier, @@ -1488,6 +1962,8 @@ public static void linkMultipleAccounts_Transaction(Start start, Connection sqlC public static void updateTimeJoinedForPrimaryUsers_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List primaryUserIds) throws SQLException, StorageQueryException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + 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 " + @@ -1514,14 +1990,18 @@ public static void updateTimeJoinedForPrimaryUsers_Transaction(Start start, Conn }); } - executeBatch(sqlCon, QUERY, usersUpdateBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, QUERY, usersUpdateBatch); + } executeBatch(sqlCon, APP_ID_QUERY, appIdUpdateBatch); } public static void unlinkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String primaryUserId, String recipeUserId) throws SQLException, StorageQueryException { - { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { 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 " + @@ -1555,8 +2035,14 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(Start start, String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { - List userIds = AccountInfoQueries.listPrimaryUserIdsByThirdPartyInfo(start, appIdentifier, - thirdPartyId, thirdPartyUserId); + List userIds; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + userIds = AccountInfoQueries.listPrimaryUserIdsByThirdPartyInfo(start, appIdentifier, + thirdPartyId, thirdPartyUserId); + } else { + userIds = ThirdPartyQueries.listPrimaryUserIdsByThirdPartyInfo_legacy(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. @@ -1572,8 +2058,14 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction( throws SQLException, StorageQueryException { // Note: Locking is now done at the core level via UserLockingStorage.lockUser() // This method just queries the users without acquiring locks. - List userIds = AccountInfoQueries.listPrimaryUserIdsByThirdPartyInfo_Transaction(start, sqlCon, - appIdentifier, thirdPartyId, thirdPartyUserId); + List userIds; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + userIds = AccountInfoQueries.listPrimaryUserIdsByThirdPartyInfo_Transaction(start, sqlCon, + appIdentifier, thirdPartyId, thirdPartyUserId); + } else { + userIds = ThirdPartyQueries.listPrimaryUserIdsByThirdPartyInfo_legacy_Transaction(start, sqlCon, + appIdentifier, thirdPartyId, thirdPartyUserId); + } List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, userIds); @@ -1586,6 +2078,15 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction( public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return listPrimaryUsersByEmail_new(start, tenantIdentifier, email); + } + return listPrimaryUsersByEmail_legacy(start, tenantIdentifier, email); + } + + private static AuthRecipeUserInfo[] listPrimaryUsersByEmail_new(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { List userIds = AccountInfoQueries.listPrimaryUserIdsByEmail(start, tenantIdentifier, email); List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(), @@ -1597,10 +2098,170 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantId return result.toArray(new AuthRecipeUserInfo[0]); } + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_legacy_forApp(Start start, AppIdentifier appIdentifier, + String email) + throws StorageQueryException, SQLException { + // App-scoped email lookup across all recipe tables (no tenant filter). + // Used for email conflict checks where cross-tenant linked accounts must be considered. + List userIds = new ArrayList<>(); + + // emailpassword_users is app-scoped + String epQuery = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id" + + " FROM " + getConfig(start).getEmailPasswordUsersTable() + " ep" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON ep.app_id = auid.app_id AND ep.user_id = auid.user_id" + + " WHERE ep.app_id = ? AND ep.email = ?"; + List epUserIds = execute(start, epQuery, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List ids = new ArrayList<>(); + while (result.next()) { + ids.add(result.getString("user_id")); + } + return ids; + }); + userIds.addAll(epUserIds); + + // thirdparty_users is app-scoped + String tpQuery = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id" + + " FROM " + getConfig(start).getThirdPartyUsersTable() + " tp" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON tp.app_id = auid.app_id AND tp.user_id = auid.user_id" + + " WHERE tp.app_id = ? AND tp.email = ?"; + List tpUserIds = execute(start, tpQuery, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List ids = new ArrayList<>(); + while (result.next()) { + ids.add(result.getString("user_id")); + } + return ids; + }); + userIds.addAll(tpUserIds); + + // passwordless_users is app-scoped + String plQuery = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id" + + " FROM " + getConfig(start).getPasswordlessUsersTable() + " pl" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON pl.app_id = auid.app_id AND pl.user_id = auid.user_id" + + " WHERE pl.app_id = ? AND pl.email = ?"; + List plUserIds = execute(start, plQuery, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List ids = new ArrayList<>(); + while (result.next()) { + ids.add(result.getString("user_id")); + } + return ids; + }); + userIds.addAll(plUserIds); + + // webauthn_users is app-scoped + String waQuery = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id" + + " FROM " + getConfig(start).getWebAuthNUsersTable() + " wa" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON wa.app_id = auid.app_id AND wa.user_id = auid.user_id" + + " WHERE wa.app_id = ? AND wa.email = ?"; + List waUserIds = execute(start, waQuery, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List ids = new ArrayList<>(); + while (result.next()) { + ids.add(result.getString("user_id")); + } + return ids; + }); + userIds.addAll(waUserIds); + + // remove duplicates + Set userIdsSet = new HashSet<>(userIds); + userIds = new ArrayList<>(userIdsSet); + + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, userIds); + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_legacy_forApp(Start start, + AppIdentifier appIdentifier, + String phoneNumber) + throws StorageQueryException, SQLException { + // App-scoped phone number lookup (passwordless_users is app-scoped) + String plQuery = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id" + + " FROM " + getConfig(start).getPasswordlessUsersTable() + " pl" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON pl.app_id = auid.app_id AND pl.user_id = auid.user_id" + + " WHERE pl.app_id = ? AND pl.phone_number = ?"; + List userIds = execute(start, plQuery, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, phoneNumber); + }, result -> { + List ids = new ArrayList<>(); + while (result.next()) { + ids.add(result.getString("user_id")); + } + return ids; + }); + + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, userIds); + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_legacy(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)); + + String webauthnUserId = WebAuthNQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email); + if(webauthnUserId != null) { + userIds.add(webauthnUserId); + } + + // 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 { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return listPrimaryUsersByPhoneNumber_new(start, tenantIdentifier, phoneNumber); + } + return listPrimaryUsersByPhoneNumber_legacy(start, tenantIdentifier, phoneNumber); + } + + private static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_new(Start start, + TenantIdentifier tenantIdentifier, + String phoneNumber) + throws StorageQueryException, SQLException { List userIds = AccountInfoQueries.listPrimaryUserIdsByPhoneNumber(start, tenantIdentifier, phoneNumber); List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(), @@ -1612,16 +2273,58 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(Start start, return result.toArray(new AuthRecipeUserInfo[0]); } + private static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_legacy(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 { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserByThirdPartyInfo_new(start, tenantIdentifier, thirdPartyId, thirdPartyUserId); + } + return getPrimaryUserByThirdPartyInfo_legacy(start, tenantIdentifier, thirdPartyId, thirdPartyUserId); + } + + private static AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo_new(Start start, + TenantIdentifier tenantIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException, SQLException { String userId = AccountInfoQueries.getPrimaryUserIdByThirdPartyInfo(start, tenantIdentifier, thirdPartyId, thirdPartyUserId); return getPrimaryUserInfoForUserId(start, tenantIdentifier.toAppIdentifier(), userId); } + private static AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo_legacy(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 AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId(Start start, TenantIdentifier tenantIdentifier, String credentialId) @@ -1690,6 +2393,16 @@ private static List getPrimaryUserInfoForUserIds(Start start AppIdentifier appIdentifier, List userIds) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserInfoForUserIds_new(start, appIdentifier, userIds); + } + return getPrimaryUserInfoForUserIds_legacy(start, appIdentifier, userIds); + } + + private static List getPrimaryUserInfoForUserIds_new(Start start, + AppIdentifier appIdentifier, + List userIds) + throws StorageQueryException, SQLException { if (userIds == null || userIds.isEmpty()){ return new ArrayList<>(); } @@ -1783,6 +2496,103 @@ private static List getPrimaryUserInfoForUserIds(Start start .collect(Collectors.toList()); } + private static List getPrimaryUserInfoForUserIds_legacy(Start start, + AppIdentifier appIdentifier, + List userIds) + throws StorageQueryException, SQLException { + if (userIds == null || userIds.isEmpty()){ + 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 primary_or_recipe_user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ")) AND app_id = ?) AND au.app_id = ?"; + + List allAuthUsersResult = execute(start, QUERY, pst -> { + // IN user_id + int index = 1; + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // IN primary_or_recipe_user_id + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // for app_id + pst.setString(index, appIdentifier.getAppId()); + pst.setString(index + 1, appIdentifier.getAppId()); + }, result -> { + List parsedResult = new ArrayList<>(); + while (result.next()) { + parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"), + result.getString("tenant_id"), + result.getString("primary_or_recipe_user_id"), + result.getBoolean("is_linked_or_is_a_primary_user"), + result.getString("recipe_id"), + result.getLong("time_joined"))); + } + 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(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll( + PasswordlessQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll(WebAuthNQueries.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 getPrimaryUserInfoForUserIds_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userIds) @@ -1795,12 +2605,19 @@ private static List getPrimaryUserInfoForUserIds_Transaction // 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 tenantJoin; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + tenantJoin = " LEFT JOIN " + getConfig(start).getRecipeUserTenantsTable() + + " as rt ON au.app_id = rt.app_id AND au.user_id = rt.recipe_user_id"; + } else { + tenantJoin = " LEFT JOIN " + getConfig(start).getUsersTable() + + " as rt ON au.app_id = rt.app_id AND au.user_id = rt.user_id"; + } String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, " + "au.time_joined, rt.tenant_id " + "FROM " + getConfig(start).getAppIdToUserIdTable() + " as au" + - " LEFT JOIN " + getConfig(start).getRecipeUserTenantsTable() + - " as rt ON au.app_id = rt.app_id AND au.user_id = rt.recipe_user_id" + + tenantJoin + " WHERE au.primary_or_recipe_user_id IN " + " (SELECT primary_or_recipe_user_id FROM " + getConfig(start).getAppIdToUserIdTable() + @@ -1901,9 +2718,18 @@ public static Map> getTenantIdsForUserIds_transaction(Start String[] userIds) throws SQLException, StorageQueryException { if (userIds != null && userIds.length > 0) { - StringBuilder QUERY = new StringBuilder("SELECT DISTINCT recipe_user_id AS user_id, tenant_id " - + "FROM " + getConfig(start).getRecipeUserTenantsTable()); - QUERY.append(" WHERE recipe_user_id IN ("); + String table; + String userIdColumn; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + table = getConfig(start).getRecipeUserTenantsTable(); + userIdColumn = "recipe_user_id"; + } else { + table = getConfig(start).getUsersTable(); + userIdColumn = "user_id"; + } + StringBuilder QUERY = new StringBuilder("SELECT DISTINCT " + userIdColumn + " AS user_id, tenant_id " + + "FROM " + table); + QUERY.append(" WHERE " + userIdColumn + " IN ("); for (int i = 0; i < userIds.length; i++) { QUERY.append("?"); @@ -1944,9 +2770,18 @@ public static Map> getTenantIdsForUserIds(Start start, String[] userIds) throws SQLException, StorageQueryException { if (userIds != null && userIds.length > 0) { - StringBuilder QUERY = new StringBuilder("SELECT DISTINCT recipe_user_id AS user_id, tenant_id " - + "FROM " + getConfig(start).getRecipeUserTenantsTable()); - QUERY.append(" WHERE recipe_user_id IN ("); + String table; + String userIdColumn; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + table = getConfig(start).getRecipeUserTenantsTable(); + userIdColumn = "recipe_user_id"; + } else { + table = getConfig(start).getUsersTable(); + userIdColumn = "user_id"; + } + StringBuilder QUERY = new StringBuilder("SELECT DISTINCT " + userIdColumn + " AS user_id, tenant_id " + + "FROM " + table); + QUERY.append(" WHERE " + userIdColumn + " IN ("); for (int i = 0; i < userIds.length; i++) { QUERY.append("?"); @@ -2072,6 +2907,14 @@ public static int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(Start sta public static boolean checkIfUsesAccountLinking(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return checkIfUsesAccountLinking_new(start, appIdentifier); + } + return checkIfUsesAccountLinking_legacy(start, appIdentifier); + } + + private static boolean checkIfUsesAccountLinking_new(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND is_linked_or_is_a_primary_user = true LIMIT 1"; @@ -2083,6 +2926,19 @@ public static boolean checkIfUsesAccountLinking(Start start, AppIdentifier appId }); } + private static boolean checkIfUsesAccountLinking_legacy(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 { @@ -2109,7 +2965,9 @@ public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, public static void updateTimeJoinedForPrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String primaryUserId) throws SQLException, StorageQueryException { - { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { 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 " + 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 289f8e92..8ace7f9b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -32,6 +32,7 @@ import javax.annotation.Nullable; import static io.supertokens.pluginInterface.RECIPE_ID.PASSWORDLESS; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.ACCOUNT_INFO_TYPE; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; @@ -281,11 +282,16 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio @Nonnull String phoneNumber, String userId) throws SQLException, StorageQueryException { + String tenantSubquery; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + tenantSubquery = "SELECT tenant_id FROM " + getConfig(start).getRecipeUserTenantsTable() + + " WHERE app_id = ? AND recipe_user_id = ?"; + } else { + tenantSubquery = "SELECT tenant_id FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + } String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() - + " WHERE app_id = ? AND phone_number = ? AND tenant_id IN (" - + " SELECT tenant_id FROM " + getConfig(start).getRecipeUserTenantsTable() - + " WHERE app_id = ? AND recipe_user_id = ?" - + ")"; + + " WHERE app_id = ? AND phone_number = ? AND tenant_id IN (" + tenantSubquery + ")"; update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -313,11 +319,16 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, @Nonnull String email, String userId) throws SQLException, StorageQueryException { + String tenantSubquery; + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + tenantSubquery = "SELECT tenant_id FROM " + getConfig(start).getRecipeUserTenantsTable() + + " WHERE app_id = ? AND recipe_user_id = ?"; + } else { + tenantSubquery = "SELECT tenant_id FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + } String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() - + " WHERE app_id = ? AND email = ? AND tenant_id IN (" - + " SELECT tenant_id FROM " + getConfig(start).getRecipeUserTenantsTable() - + " WHERE app_id = ? AND recipe_user_id = ?" - + ")"; + + " WHERE app_id = ? AND email = ? AND tenant_id IN (" + tenantSubquery + ")"; update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -425,7 +436,9 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - { // app_id_to_user_id + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + { // app_id_to_user_id — ALWAYS String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?)"; @@ -439,7 +452,7 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant }); } - { // all_auth_recipe_users + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + @@ -456,7 +469,7 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant }); } - { // recipe_user_tenants + if (mode.writesToNewTables()) { // recipe_user_tenants if (email != null) { AccountInfoQueries.addRecipeUserAccountInfo_Transaction(start, sqlCon, tenantIdentifier, id, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", email); @@ -470,7 +483,7 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant } } - { // passwordless_users + { // passwordless_users — ALWAYS String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUsersTable() + "(app_id, user_id, email, phone_number, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { @@ -482,7 +495,7 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant }); } - { // passwordless_user_to_tenant + if (mode.writesToOldTables()) { // passwordless_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + "(app_id, tenant_id, user_id, email, phone_number)" + " VALUES(?, ?, ?, ?, ?)"; @@ -510,6 +523,44 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getUserInfosWithTenant_Transaction_new(start, con, appIdentifier, userId); + } + return getUserInfosWithTenant_Transaction_legacy(start, con, appIdentifier, userId); + } + + private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction_legacy(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 " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " + + "JOIN " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant " + + "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.user_id = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + List userInfos = new ArrayList<>(); + + while (result.next()) { + userInfos.add(new UserInfoWithTenantId( + result.getString("user_id"), + result.getString("tenant_id"), + result.getString("email"), + result.getString("phone_number") + )); + } + return userInfos.toArray(new UserInfoWithTenantId[0]); + }); + } + + private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction_new(Start start, Connection con, + AppIdentifier appIdentifier, + String userId) + throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT pl_users.user_id as user_id, pl_users.email as email, " + "pl_users.phone_number as phone_number, rut.tenant_id as tenant_id " + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " @@ -537,9 +588,12 @@ private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction(Start s public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, String userId, boolean deleteUserIdMappingToo) throws StorageQueryException, SQLException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + UserInfoWithTenantId[] userInfos = getUserInfosWithTenant_Transaction(start, sqlCon, appIdentifier, userId); if (deleteUserIdMappingToo) { + // Deleting from app_id_to_user_id cascades to all child tables String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; @@ -548,7 +602,7 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde pst.setString(2, userId); }); } else { - { + if (mode.writesToOldTables()) { String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { @@ -557,7 +611,7 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde }); } - { + { // passwordless_users — ALWAYS String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { @@ -588,7 +642,9 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String email) throws SQLException, StorageQueryException { - { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { // passwordless_user_to_tenant String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable() + " SET email = ? WHERE app_id = ? AND user_id = ?"; @@ -598,7 +654,7 @@ public static int updateUserEmail_Transaction(Start start, Connection con, AppId pst.setString(3, userId); }); } - { + { // passwordless_users — ALWAYS String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUsersTable() + " SET email = ? WHERE app_id = ? AND user_id = ?"; @@ -613,7 +669,9 @@ 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) throws SQLException, StorageQueryException { - { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { // passwordless_user_to_tenant String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable() + " SET phone_number = ? WHERE app_id = ? AND user_id = ?"; @@ -623,7 +681,7 @@ public static int updateUserPhoneNumber_Transaction(Start start, Connection con, pst.setString(3, userId); }); } - { + { // passwordless_users — ALWAYS String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUsersTable() + " SET phone_number = ? WHERE app_id = ? AND user_id = ?"; @@ -843,6 +901,36 @@ private static UserInfoPartial getUserById_Transaction(Start start, Connection s public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserIdUsingEmail_new(start, tenantIdentifier, email); + } + return getPrimaryUserIdUsingEmail_legacy(start, tenantIdentifier, email); + } + + private static String getPrimaryUserIdUsingEmail_legacy(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).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 = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + } + + private static String getPrimaryUserIdUsingEmail_new(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserTenantsTable() + " AS rut" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS auid" + @@ -865,6 +953,36 @@ public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier te public static String getPrimaryUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserByPhoneNumber_new(start, tenantIdentifier, phoneNumber); + } + return getPrimaryUserByPhoneNumber_legacy(start, tenantIdentifier, phoneNumber); + } + + private static String getPrimaryUserByPhoneNumber_legacy(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 result.getString("user_id"); + } + return null; + }); + } + + private static String getPrimaryUserByPhoneNumber_new(Start start, TenantIdentifier tenantIdentifier, + @Nonnull String phoneNumber) + throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserTenantsTable() + " AS rut" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS auid" + @@ -894,10 +1012,12 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC throw new UnknownUserIdException(); } + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); - { // all_auth_recipe_users + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, " + @@ -918,7 +1038,7 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC accountLinkingInfo.primaryUserId); } - { // passwordless_user_to_tenant + if (mode.writesToOldTables()) { // passwordless_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + "(app_id, tenant_id, user_id, email, phone_number)" + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT ON CONSTRAINT " @@ -936,12 +1056,16 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC return numRows > 0; } + + return true; } public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - { // all_auth_recipe_users + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND tenant_id = ? and user_id = ? and recipe_id = ?"; int numRows = update(sqlCon, QUERY, pst -> { @@ -952,9 +1076,10 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection }); return numRows > 0; + // automatically deleted from passwordless_user_to_tenant because of foreign key constraint } - // automatically deleted from passwordless_user_to_tenant because of foreign key constraint + return true; } private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, @@ -1080,6 +1205,7 @@ private static List fillUserInfoWithTenantIds(Start start, public static void importUsers_Transaction(Connection sqlCon, Start start, Collection users) throws SQLException, StorageQueryException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); String app_id_to_user_id_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + "(app_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)" @@ -1109,20 +1235,22 @@ public static void importUsers_Transaction(Connection sqlCon, Start start, String primaryOrRecipeUserId = user.primaryUserId != null ? user.primaryUserId : user.userId; boolean isLinkedOrIsPrimaryUser = user.primaryUserId != null; - // Recipe Account Info - if (user.email != null) { - AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); - } - if (user.phoneNumber != null) { - AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.PHONE_NUMBER, "", "", user.phoneNumber, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); - } + if (mode.writesToNewTables()) { + // Recipe Account Info + if (user.email != null) { + AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); + } + if (user.phoneNumber != null) { + AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.PHONE_NUMBER, "", "", user.phoneNumber, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); + } - // Recipe User Tenants - if (user.email != null) { - AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, user.recipeUserTenantIds); - } - if (user.phoneNumber != null) { - AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.PHONE_NUMBER, "", "", user.phoneNumber, user.recipeUserTenantIds); + // Recipe User Tenants + if (user.email != null) { + AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, user.recipeUserTenantIds); + } + if (user.phoneNumber != null) { + AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, PASSWORDLESS.toString(), ACCOUNT_INFO_TYPE.PHONE_NUMBER, "", "", user.phoneNumber, user.recipeUserTenantIds); + } } appIdToUserIdBatch.add(pst -> { @@ -1145,34 +1273,42 @@ public static void importUsers_Transaction(Connection sqlCon, Start start, // Generate entries for all recipe user tenant IDs for (String tenantId : user.recipeUserTenantIds) { - allAuthRecipeUsersBatch.add(pst -> { - pst.setString(1, appId); - pst.setString(2, tenantId); - pst.setString(3, user.userId); - pst.setString(4, primaryOrRecipeUserId); - pst.setBoolean(5, isLinkedOrIsPrimaryUser); - pst.setString(6, PASSWORDLESS.toString()); - pst.setLong(7, user.timeJoinedMSSinceEpoch); - pst.setLong(8, user.timeJoinedMSSinceEpoch); - }); + if (mode.writesToOldTables()) { + allAuthRecipeUsersBatch.add(pst -> { + pst.setString(1, appId); + pst.setString(2, tenantId); + pst.setString(3, user.userId); + pst.setString(4, primaryOrRecipeUserId); + pst.setBoolean(5, isLinkedOrIsPrimaryUser); + pst.setString(6, PASSWORDLESS.toString()); + pst.setLong(7, user.timeJoinedMSSinceEpoch); + pst.setLong(8, user.timeJoinedMSSinceEpoch); + }); - passwordlessUserToTenantBatch.add(pst -> { - pst.setString(1, appId); - pst.setString(2, tenantId); - pst.setString(3, user.userId); - pst.setString(4, user.email); - pst.setString(5, user.phoneNumber); - }); + passwordlessUserToTenantBatch.add(pst -> { + pst.setString(1, appId); + pst.setString(2, tenantId); + pst.setString(3, user.userId); + pst.setString(4, user.email); + pst.setString(5, user.phoneNumber); + }); + } } } - executeBatch(sqlCon, AccountInfoQueries.getRecipeUserAccountInfoBatchQuery(start), recipeUserAccountInfoBatch); - executeBatch(sqlCon, AccountInfoQueries.getRecipeUserTenantBatchQuery(start), recipeUserTenantsBatch); + if (mode.writesToNewTables()) { + executeBatch(sqlCon, AccountInfoQueries.getRecipeUserAccountInfoBatchQuery(start), recipeUserAccountInfoBatch); + executeBatch(sqlCon, AccountInfoQueries.getRecipeUserTenantBatchQuery(start), recipeUserTenantsBatch); + } executeBatch(sqlCon, app_id_to_user_id_QUERY, appIdToUserIdBatch); - executeBatch(sqlCon, all_auth_recipe_users_QUERY, allAuthRecipeUsersBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, all_auth_recipe_users_QUERY, allAuthRecipeUsersBatch); + } executeBatch(sqlCon, passwordless_users_QUERY, passwordlessUsersBatch); - executeBatch(sqlCon, passwordless_user_to_tenant_QUERY, passwordlessUserToTenantBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlCon, passwordless_user_to_tenant_QUERY, passwordlessUserToTenantBatch); + } } private static class PasswordlessDeviceRowMapper implements RowMapper { 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 b08ee2ae..35b844be 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -134,6 +134,32 @@ public static void createNewSession(Start start, TenantIdentifier tenantIdentifi public static SessionInfo getSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String sessionHandle) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getSessionInfo_Transaction_new(start, con, tenantIdentifier, sessionHandle); + } + return getSessionInfo_Transaction_legacy(start, con, tenantIdentifier, sessionHandle); + } + + private static SessionInfo getSessionInfo_Transaction_legacy(Start start, Connection con, + TenantIdentifier tenantIdentifier, + String sessionHandle) + throws SQLException, StorageQueryException { + return getSessionInfo_Transaction_impl(start, con, tenantIdentifier, sessionHandle, + getConfig(start).getUsersTable()); + } + + private static SessionInfo getSessionInfo_Transaction_new(Start start, Connection con, + TenantIdentifier tenantIdentifier, + String sessionHandle) + throws SQLException, StorageQueryException { + return getSessionInfo_Transaction_impl(start, con, tenantIdentifier, sessionHandle, + getConfig(start).getAppIdToUserIdTable()); + } + + private static SessionInfo getSessionInfo_Transaction_impl(Start start, Connection con, + TenantIdentifier tenantIdentifier, + String sessionHandle, String userIdTable) + 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 " + @@ -158,7 +184,7 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con "FROM " + getConfig(start).getUserIdMappingTable() + " um2 " + "WHERE um2.app_id = ? AND um2.supertokens_user_id IN (" + "SELECT primary_or_recipe_user_id " + - "FROM " + getConfig(start).getAppIdToUserIdTable() + " " + + "FROM " + userIdTable + " " + "WHERE app_id = ? AND user_id IN (" + "SELECT user_id FROM (" + "SELECT um1.supertokens_user_id as user_id, 0 as o1 " + @@ -172,7 +198,7 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con ") " + "UNION " + "SELECT primary_or_recipe_user_id, 1 as o " + - "FROM " + getConfig(start).getAppIdToUserIdTable() + " " + + "FROM " + userIdTable + " " + "WHERE app_id = ? AND user_id IN (" + "SELECT user_ID FROM (" + "SELECT um1.supertokens_user_id as user_id, 0 as o2 " + @@ -423,6 +449,25 @@ public static int updateSession(Start start, TenantIdentifier tenantIdentifier, public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getSession_new(start, tenantIdentifier, sessionHandle); + } + return getSession_legacy(start, tenantIdentifier, sessionHandle); + } + + private static SessionInfo getSession_legacy(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) + throws SQLException, StorageQueryException { + return getSession_impl(start, tenantIdentifier, sessionHandle, getConfig(start).getUsersTable()); + } + + private static SessionInfo getSession_new(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) + throws SQLException, StorageQueryException { + return getSession_impl(start, tenantIdentifier, sessionHandle, getConfig(start).getAppIdToUserIdTable()); + } + + private static SessionInfo getSession_impl(Start start, TenantIdentifier tenantIdentifier, String sessionHandle, + String userIdTable) + throws SQLException, StorageQueryException { String QUERY = "SELECT sess.session_handle, sess.user_id, sess.refresh_token_hash_2, sess.session_data, sess" + ".expires_at, " @@ -430,7 +475,7 @@ public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentif "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).getAppIdToUserIdTable() + + + " AS sess LEFT JOIN " + userIdTable + " 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 = ?"; 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 00b29ec4..82ad7b78 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import static io.supertokens.pluginInterface.RECIPE_ID.THIRD_PARTY; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.ACCOUNT_INFO_TYPE; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; @@ -116,6 +117,8 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" @@ -130,7 +133,7 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } - { // all_auth_recipe_users + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + @@ -147,7 +150,7 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } - { // recipe_user_tenants + if (mode.writesToNewTables()) { // recipe_user_tenants // Insert row for email AccountInfoQueries.addRecipeUserAccountInfo_Transaction(start, sqlCon, tenantIdentifier, id, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.EMAIL, thirdParty.id, thirdParty.userId, email); @@ -172,7 +175,7 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } - { // thirdparty_user_to_tenant + if (mode.writesToOldTables()) { // 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(?, ?, ?, ?, ?)"; @@ -200,6 +203,8 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, String userId, boolean deleteUserIdMappingToo) throws StorageQueryException, SQLException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + if (deleteUserIdMappingToo) { String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; @@ -209,7 +214,7 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde pst.setString(2, userId); }); } else { - { + if (mode.writesToOldTables()) { String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND user_id = ?"; update(sqlCon, QUERY, pst -> { @@ -377,7 +382,37 @@ public static List listUserIdsByMultipleThirdPartyInfo_Transaction(Start public static String getUserIdByThirdPartyInfo(Start start, TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getUserIdByThirdPartyInfo_new(start, tenantIdentifier, thirdPartyId, thirdPartyUserId); + } + return getUserIdByThirdPartyInfo_legacy(start, tenantIdentifier, thirdPartyId, thirdPartyUserId); + } + + private static String getUserIdByThirdPartyInfo_legacy(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, tp.user_id AS recipe_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 result.getString("user_id"); + } + return null; + }); + } + private static String getUserIdByThirdPartyInfo_new(Start start, TenantIdentifier tenantIdentifier, + String thirdPartyId, String thirdPartyUserId) + throws SQLException, StorageQueryException { String QUERY = "SELECT DISTINCT a.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserTenantsTable() + " AS rut" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS a" @@ -436,6 +471,39 @@ private static UserInfoPartial getUserInfoUsingUserId_Transaction(Start start, C public static List getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserIdUsingEmail_new(start, tenantIdentifier, email); + } + return getPrimaryUserIdUsingEmail_legacy(start, tenantIdentifier, email); + } + + private static List getPrimaryUserIdUsingEmail_legacy(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<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); + } + return finalResult; + }); + } + + private static List getPrimaryUserIdUsingEmail_new(Start start, + TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT a.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserTenantsTable() + " AS rut" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS a" @@ -460,6 +528,8 @@ public static List getPrimaryUserIdUsingEmail(Start start, public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException, UnknownUserIdException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); @@ -470,7 +540,7 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); - { // all_auth_recipe_users + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, " + @@ -491,7 +561,7 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC accountLinkingInfo.primaryUserId); } - { // thirdparty_user_to_tenant + if (mode.writesToOldTables()) { // 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 ON CONSTRAINT " @@ -508,12 +578,16 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC return numRows > 0; } + + return false; } public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - { // all_auth_recipe_users + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { // all_auth_recipe_users String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + " WHERE app_id = ? AND tenant_id = ? and user_id = ? and recipe_id = ?"; int numRows = update(sqlCon, QUERY, pst -> { @@ -527,10 +601,12 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection } // automatically deleted from thirdparty_user_to_tenant because of foreign key constraint + return false; } public static void importUser_Transaction(Start start, Connection sqlConnection, Collection users) throws SQLException, StorageQueryException { + MigrationMode mode = Config.getConfig(start).getMigrationMode(); String app_id_userid_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + "(app_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)" @@ -563,13 +639,15 @@ public static void importUser_Transaction(Start start, Connection sqlConnection, String primaryOrRecipeUserId = user.primaryUserId != null ? user.primaryUserId : user.userId; boolean isLinkedOrIsPrimaryUser = user.primaryUserId != null; - // Recipe Account Info - AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.THIRD_PARTY, "", "", new LoginMethod.ThirdParty(user.thirdpartyId, user.thirdpartyUserId).getAccountInfoValue(), isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); - AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.EMAIL, user.thirdpartyId, user.thirdpartyUserId, user.email, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); + if (mode.writesToNewTables()) { + // Recipe Account Info + AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.THIRD_PARTY, "", "", new LoginMethod.ThirdParty(user.thirdpartyId, user.thirdpartyUserId).getAccountInfoValue(), isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); + AccountInfoQueries.addRecipeUserAccountInfoToBatch(recipeUserAccountInfoBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.EMAIL, user.thirdpartyId, user.thirdpartyUserId, user.email, isLinkedOrIsPrimaryUser ? primaryOrRecipeUserId : null); - // Recipe User Tenants - AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.THIRD_PARTY, "", "", new LoginMethod.ThirdParty(user.thirdpartyId, user.thirdpartyUserId).getAccountInfoValue(), user.recipeUserTenantIds); - AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, user.recipeUserTenantIds); + // Recipe User Tenants + AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.THIRD_PARTY, "", "", new LoginMethod.ThirdParty(user.thirdpartyId, user.thirdpartyUserId).getAccountInfoValue(), user.recipeUserTenantIds); + AccountInfoQueries.addRecipeUserTenantsToBatch(recipeUserTenantsBatch, user.appIdentifier, user.userId, THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", user.email, user.recipeUserTenantIds); + } appIdToUserIdBatch.add(pst -> { pst.setString(1, appId); @@ -592,34 +670,42 @@ public static void importUser_Transaction(Start start, Connection sqlConnection, // Generate entries for all recipe user tenant IDs for (String tenantId : user.recipeUserTenantIds) { - allAuthRecipeUsersBatch.add(pst -> { - pst.setString(1, appId); - pst.setString(2, tenantId); - pst.setString(3, user.userId); - pst.setString(4, primaryOrRecipeUserId); - pst.setBoolean(5, isLinkedOrIsPrimaryUser); - pst.setString(6, THIRD_PARTY.toString()); - pst.setLong(7, user.timeJoinedMSSinceEpoch); - pst.setLong(8, user.timeJoinedMSSinceEpoch); - }); + if (mode.writesToOldTables()) { + allAuthRecipeUsersBatch.add(pst -> { + pst.setString(1, appId); + pst.setString(2, tenantId); + pst.setString(3, user.userId); + pst.setString(4, primaryOrRecipeUserId); + pst.setBoolean(5, isLinkedOrIsPrimaryUser); + pst.setString(6, THIRD_PARTY.toString()); + pst.setLong(7, user.timeJoinedMSSinceEpoch); + pst.setLong(8, user.timeJoinedMSSinceEpoch); + }); - thirdPartyUsersToTenantBatch.add(pst -> { - pst.setString(1, appId); - pst.setString(2, tenantId); - pst.setString(3, user.userId); - pst.setString(4, user.thirdpartyId); - pst.setString(5, user.thirdpartyUserId); - }); + thirdPartyUsersToTenantBatch.add(pst -> { + pst.setString(1, appId); + pst.setString(2, tenantId); + pst.setString(3, user.userId); + pst.setString(4, user.thirdpartyId); + pst.setString(5, user.thirdpartyUserId); + }); + } } } - executeBatch(sqlConnection, AccountInfoQueries.getRecipeUserAccountInfoBatchQuery(start), recipeUserAccountInfoBatch); - executeBatch(sqlConnection, AccountInfoQueries.getRecipeUserTenantBatchQuery(start), recipeUserTenantsBatch); + if (mode.writesToNewTables()) { + executeBatch(sqlConnection, AccountInfoQueries.getRecipeUserAccountInfoBatchQuery(start), recipeUserAccountInfoBatch); + executeBatch(sqlConnection, AccountInfoQueries.getRecipeUserTenantBatchQuery(start), recipeUserTenantsBatch); + } executeBatch(sqlConnection, app_id_userid_QUERY, appIdToUserIdBatch); - executeBatch(sqlConnection, all_auth_recipe_users_QUERY, allAuthRecipeUsersBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlConnection, all_auth_recipe_users_QUERY, allAuthRecipeUsersBatch); + } executeBatch(sqlConnection, thirdparty_users_QUERY, thirdPartyUsersBatch); - executeBatch(sqlConnection, thirdparty_user_to_tenant_QUERY, thirdPartyUsersToTenantBatch); + if (mode.writesToOldTables()) { + executeBatch(sqlConnection, thirdparty_user_to_tenant_QUERY, thirdPartyUsersToTenantBatch); + } } private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, @@ -720,4 +806,50 @@ public UserInfoPartial map(ResultSet result) throws Exception { result.getLong("time_joined")); } } + + public static List listPrimaryUserIdsByThirdPartyInfo_legacy(Start start, AppIdentifier appIdentifier, + String thirdPartyId, String thirdPartyUserId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id" + + " FROM " + getConfig(start).getThirdPartyUsersTable() + " tp" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON tp.app_id = auid.app_id AND tp.user_id = auid.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; + + 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("primary_or_recipe_user_id")); + } + return userIds; + }); + } + + public static List listPrimaryUserIdsByThirdPartyInfo_legacy_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id" + + " FROM " + getConfig(start).getThirdPartyUsersTable() + " tp" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " auid" + + " ON tp.app_id = auid.app_id AND tp.user_id = auid.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; + + return execute(sqlCon, 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("primary_or_recipe_user_id")); + } + return userIds; + }); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/WebAuthNQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/WebAuthNQueries.java index 6490bd2d..ee0ed2ba 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/WebAuthNQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/WebAuthNQueries.java @@ -30,6 +30,7 @@ import org.jetbrains.annotations.Nullable; +import io.supertokens.pluginInterface.MigrationMode; import io.supertokens.pluginInterface.authRecipe.ACCOUNT_INFO_TYPE; import static io.supertokens.pluginInterface.RECIPE_ID.WEBAUTHN; import io.supertokens.pluginInterface.RowMapper; @@ -45,6 +46,7 @@ import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.Config; import static io.supertokens.storage.postgresql.config.Config.getConfig; import io.supertokens.storage.postgresql.utils.Utils; @@ -297,6 +299,7 @@ public static void createUser_Transaction(Start start, Connection sqlCon, Tenant String relyingPartyId) throws StorageTransactionLogicException, StorageQueryException { long timeJoined = System.currentTimeMillis(); + MigrationMode mode = Config.getConfig(start).getMigrationMode(); try { // app_id_to_user_id @@ -312,38 +315,41 @@ public static void createUser_Transaction(Start start, Connection sqlCon, Tenant pst.setLong(6, timeJoined); }); - // all_auth_recipe_users - String insertAllAuthRecipeUsers = "INSERT INTO " + getConfig(start).getUsersTable() - + - "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + - "primary_or_recipe_user_time_joined)" + - " VALUES(?, ?, ?, ?, ?, ?, ?)"; - update(sqlCon, insertAllAuthRecipeUsers, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userId); - pst.setString(4, userId); - pst.setString(5, WEBAUTHN.toString()); - pst.setLong(6, timeJoined); - pst.setLong(7, timeJoined); - }); - - // recipe_user_tenants - AccountInfoQueries.addRecipeUserAccountInfo_Transaction(start, sqlCon, tenantIdentifier, userId, - WEBAUTHN.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", email); + if (mode.writesToOldTables()) { // all_auth_recipe_users + String insertAllAuthRecipeUsers = "INSERT INTO " + getConfig(start).getUsersTable() + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + + "primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; + update(sqlCon, insertAllAuthRecipeUsers, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, userId); + pst.setString(5, WEBAUTHN.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); + }); + } - // webauthn_user_to_tenant - String insertWebauthNUsersToTenant = - "INSERT INTO " + getConfig(start).getWebAuthNUserToTenantTable() - + " (app_id, tenant_id, user_id, email) " - + " VALUES (?,?,?,?);"; + if (mode.writesToNewTables()) { // recipe_user_tenants + AccountInfoQueries.addRecipeUserAccountInfo_Transaction(start, sqlCon, tenantIdentifier, userId, + WEBAUTHN.toString(), ACCOUNT_INFO_TYPE.EMAIL, "", "", email); + } - update(sqlCon, insertWebauthNUsersToTenant, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setString(3, userId); - pst.setString(4, email); - }); + if (mode.writesToOldTables()) { // webauthn_user_to_tenant + String insertWebauthNUsersToTenant = + "INSERT INTO " + getConfig(start).getWebAuthNUserToTenantTable() + + " (app_id, tenant_id, user_id, email) " + + " VALUES (?,?,?,?);"; + + update(sqlCon, insertWebauthNUsersToTenant, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setString(3, userId); + pst.setString(4, email); + }); + } // webauthn_users String insertWebauthNUsers = "INSERT INTO " + getConfig(start).getWebAuthNUsersTable() @@ -427,6 +433,40 @@ public static String getPrimaryUserIdForTenantUsingEmail_Transaction(Start start TenantIdentifier tenantIdentifier, String email) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserIdForTenantUsingEmail_Transaction_new(start, sqlConnection, tenantIdentifier, email); + } + return getPrimaryUserIdForTenantUsingEmail_Transaction_legacy(start, sqlConnection, tenantIdentifier, email); + } + + private static String getPrimaryUserIdForTenantUsingEmail_Transaction_legacy(Start start, Connection sqlConnection, + TenantIdentifier tenantIdentifier, + String email) + throws SQLException, StorageQueryException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " AS webauthn" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON webauthn.tenant_id = all_users.tenant_id" + + " AND webauthn.app_id = all_users.app_id" + + " AND webauthn.user_id = all_users.user_id" + + " WHERE webauthn.tenant_id = ? AND webauthn.app_id = ? AND webauthn.email = ?"; + + return execute(sqlConnection, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getTenantId()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + } + + private static String getPrimaryUserIdForTenantUsingEmail_Transaction_new(Start start, Connection sqlConnection, + TenantIdentifier tenantIdentifier, + String email) + throws SQLException, StorageQueryException { String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserTenantsTable() + " AS rut" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS auid" @@ -449,6 +489,35 @@ public static String getPrimaryUserIdForTenantUsingEmail_Transaction(Start start public static String getPrimaryUserIdForAppUsingEmail_Transaction(Start start, Connection sqlConnection, AppIdentifier appIdentifier, String email) throws SQLException, StorageQueryException { + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserIdForAppUsingEmail_Transaction_new(start, sqlConnection, appIdentifier, email); + } + return getPrimaryUserIdForAppUsingEmail_Transaction_legacy(start, sqlConnection, appIdentifier, email); + } + + private static String getPrimaryUserIdForAppUsingEmail_Transaction_legacy(Start start, Connection sqlConnection, + AppIdentifier appIdentifier, String email) + throws SQLException, StorageQueryException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " AS webauthn" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON webauthn.user_id = all_users.user_id" + + " WHERE webauthn.app_id = ? AND webauthn.email = ?"; + + return execute(sqlConnection, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + } + + private static String getPrimaryUserIdForAppUsingEmail_Transaction_new(Start start, Connection sqlConnection, + AppIdentifier appIdentifier, String email) + throws SQLException, StorageQueryException { String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserAccountInfosTable() + " AS ruai" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS auid" @@ -473,6 +542,39 @@ public static List getPrimaryUserIdsUsingEmails_Transaction(Start start, if(emails == null || emails.isEmpty()) { return new ArrayList<>(); } + if (Config.getConfig(start).getMigrationMode().readsFromNewTables()) { + return getPrimaryUserIdsUsingEmails_Transaction_new(start, sqlConnection, appIdentifier, emails); + } + return getPrimaryUserIdsUsingEmails_Transaction_legacy(start, sqlConnection, appIdentifier, emails); + } + + private static List getPrimaryUserIdsUsingEmails_Transaction_legacy(Start start, Connection sqlConnection, + AppIdentifier appIdentifier, List emails) + throws SQLException, StorageQueryException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " 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.email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; + + return execute(sqlConnection, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int i = 2; + for(String email : emails) { + pst.setString(i++, email); + } + }, result -> { + List idResult = new ArrayList<>(); + if (result.next()) { + idResult.add(result.getString("user_id")); + } + return idResult; + }); + } + + private static List getPrimaryUserIdsUsingEmails_Transaction_new(Start start, Connection sqlConnection, + AppIdentifier appIdentifier, List emails) + throws SQLException, StorageQueryException { String QUERY = "SELECT DISTINCT auid.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getRecipeUserAccountInfosTable() + " AS ruai" + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS auid" @@ -692,19 +794,25 @@ public static void updateUserEmail(Start start, TenantIdentifier tenantIdentifie public static void updateUserEmail_Transaction(Start start, Connection sqlConnection, TenantIdentifier tenantIdentifier, String userId, String newEmail) throws StorageQueryException { try { - String UPDATE_USER_TO_TENANT_QUERY = - "UPDATE " + getConfig(start).getWebAuthNUserToTenantTable() + - " SET email = ? WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + MigrationMode mode = Config.getConfig(start).getMigrationMode(); + + if (mode.writesToOldTables()) { // webauthn_user_to_tenant + String UPDATE_USER_TO_TENANT_QUERY = + "UPDATE " + getConfig(start).getWebAuthNUserToTenantTable() + + " SET email = ? WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + update(sqlConnection, UPDATE_USER_TO_TENANT_QUERY, pst -> { + pst.setString(1, newEmail); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + pst.setString(4, userId); + }); + } + + // webauthn_users - ALWAYS String UPDATE_USER_QUERY = "UPDATE " + getConfig(start).getWebAuthNUsersTable() + " SET email = ? WHERE app_id = ? AND user_id = ?"; - update(sqlConnection, UPDATE_USER_TO_TENANT_QUERY, pst -> { - pst.setString(1, newEmail); - pst.setString(2, tenantIdentifier.getAppId()); - pst.setString(3, tenantIdentifier.getTenantId()); - pst.setString(4, userId); - }); - update(sqlConnection, UPDATE_USER_QUERY, pst -> { pst.setString(1, newEmail); pst.setString(2, tenantIdentifier.getAppId());