diff --git a/.circleci/config.yml b/.circleci/config.yml index 8fd9177a..5bcca6e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ jobs: test: docker: - image: rishabhpoddar/supertokens_postgresql_plugin_test + - image: rishabhpoddar/oauth-server-cicd resource_class: large steps: - add_ssh_keys: diff --git a/.circleci/doOneMillionUsersTests.sh b/.circleci/doOneMillionUsersTests.sh index ec82508d..2b4ca15d 100755 --- a/.circleci/doOneMillionUsersTests.sh +++ b/.circleci/doOneMillionUsersTests.sh @@ -93,6 +93,7 @@ do cd ../../ git clone git@github.com:supertokens/supertokens-root.git cd supertokens-root + rm gradle.properties update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2 update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2 diff --git a/.circleci/doTests.sh b/.circleci/doTests.sh index 554e23ae..6aae469b 100755 --- a/.circleci/doTests.sh +++ b/.circleci/doTests.sh @@ -93,6 +93,7 @@ do cd ../../ git clone git@github.com:supertokens/supertokens-root.git cd supertokens-root + rm gradle.properties update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2 update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4de1c9d4..2b22ae85 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,27 +1,38 @@ ## Summary of change + (A few sentences about this PR) ## Related issues + - Link to issue1 here - Link to issue1 here ## Test Plan -(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Bonus points for screenshots and videos!) + +(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your +changes work. Bonus points for screenshots and videos!) ## Documentation changes -(If relevant, please create a PR in our [docs repo](https://github.com/supertokens/docs), or create a checklist here highlighting the necessary changes) + +(If relevant, please create a PR in our [docs repo](https://github.com/supertokens/docs), or create a checklist here +highlighting the necessary changes) ## Checklist for important updates + - [ ] Changelog has been updated - [ ] `pluginInterfaceSupported.json` file has been updated (if needed) - [ ] Changes to the version if needed - - In `build.gradle` + - In `build.gradle` - [ ] Had installed and ran the pre-commit hook -- [ ] If there are new dependencies that have been added in `build.gradle`, please make sure to add them in `implementationDependencies.json`. +- [ ] If there are new dependencies that have been added in `build.gradle`, please make sure to add them + in `implementationDependencies.json`. - [ ] Issue this PR against the latest non released version branch. - - To know which one it is, run find the latest released tag (`git tag`) in the format `vX.Y.Z`, and then find the latest branch (`git branch --all`) whose `X.Y` is greater than the latest released tag. - - If no such branch exists, then create one from the latest released branch. + - To know which one it is, run find the latest released tag (`git tag`) in the format `vX.Y.Z`, and then find the + latest branch (`git branch --all`) whose `X.Y` is greater than the latest released tag. + - If no such branch exists, then create one from the latest released branch. - [ ] When adding new recipes, ensure that its performance is being measured in the `OneMillionUsersTest` + ## Remaining TODOs for this PR + - [ ] Item1 - [ ] Item2 \ No newline at end of file diff --git a/.github/helpers/package.json b/.github/helpers/package.json index 80ec546b..28051cdb 100644 --- a/.github/helpers/package.json +++ b/.github/helpers/package.json @@ -1,14 +1,14 @@ { - "name": "helpers", - "version": "1.0.0", - "description": "", - "main": "test-pass-check-pr.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "github-workflow-helpers": "github:supertokens/github-workflow-helpers" - } + "name": "helpers", + "version": "1.0.0", + "description": "", + "main": "test-pass-check-pr.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "github-workflow-helpers": "github:supertokens/github-workflow-helpers" + } } \ No newline at end of file diff --git a/.github/workflows/github-actions-changelog.yml b/.github/workflows/github-actions-changelog.yml index 0007ca9a..47aaf4a2 100644 --- a/.github/workflows/github-actions-changelog.yml +++ b/.github/workflows/github-actions-changelog.yml @@ -1,15 +1,15 @@ name: "Enforcing changelog in PRs Workflow" on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] + types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ] jobs: # Enforces the update of a changelog file on every pull request changelog: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dangoslen/changelog-enforcer@v2 - with: - changeLogPath: 'CHANGELOG.md' - skipLabels: 'Skip-Changelog' \ No newline at end of file + - uses: actions/checkout@v2 + - uses: dangoslen/changelog-enforcer@v2 + with: + changeLogPath: 'CHANGELOG.md' + skipLabels: 'Skip-Changelog' \ No newline at end of file diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 904efde8..96281a74 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -1,20 +1,20 @@ name: "Lint PR Title" on: - pull_request: - types: - - opened - - reopened - - edited - - synchronize + pull_request: + types: + - opened + - reopened + - edited + - synchronize jobs: - pr-title: - name: Lint PR title - runs-on: ubuntu-latest - steps: - - uses: amannn/action-semantic-pull-request@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - validateSingleCommit: true \ No newline at end of file + pr-title: + name: Lint PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: true \ No newline at end of file diff --git a/.github/workflows/tests-pass-check-pr.yml b/.github/workflows/tests-pass-check-pr.yml index 4f03705b..07467d9d 100644 --- a/.github/workflows/tests-pass-check-pr.yml +++ b/.github/workflows/tests-pass-check-pr.yml @@ -1,24 +1,24 @@ name: "Check if \"Run tests\" action succeeded" on: - pull_request: - types: - - opened - - reopened - - edited - - synchronize + pull_request: + types: + - opened + - reopened + - edited + - synchronize jobs: - pr-run-test-action: - name: Check if "Run tests" action succeeded - timeout-minutes: 60 - concurrency: - group: ${{ github.head_ref }} - cancel-in-progress: true - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: node install - run: cd ./.github/helpers && npm i - - name: Calling github API - run: cd ./.github/helpers && GITHUB_TOKEN=${{ github.token }} REPO=${{ github.repository }} RUN_ID=${{ github.run_id }} BRANCH=${{ github.head_ref }} JOB_ID=${{ github.job }} SOURCE_OWNER=${{ github.event.pull_request.head.repo.owner.login }} CURRENT_SHA=${{ github.event.pull_request.head.sha }} node node_modules/github-workflow-helpers/test-pass-check-pr.js \ No newline at end of file + pr-run-test-action: + name: Check if "Run tests" action succeeded + timeout-minutes: 60 + concurrency: + group: ${{ github.head_ref }} + cancel-in-progress: true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: node install + run: cd ./.github/helpers && npm i + - name: Calling github API + run: cd ./.github/helpers && GITHUB_TOKEN=${{ github.token }} REPO=${{ github.repository }} RUN_ID=${{ github.run_id }} BRANCH=${{ github.head_ref }} JOB_ID=${{ github.job }} SOURCE_OWNER=${{ github.event.pull_request.head.repo.owner.login }} CURRENT_SHA=${{ github.event.pull_request.head.sha }} node node_modules/github-workflow-helpers/test-pass-check-pr.js \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8bb11ba3..dc2133e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,19 +4,19 @@ on: inputs: coreRepoOwnerName: description: 'supertokens-core repo owner name' - default: supertokens + default: supertokens required: true coreRepoBranch: description: 'supertokens-core repos branch name' - default: master + default: master required: true pluginRepoOwnerName: description: 'supertokens-plugin-interface repo owner name' - default: supertokens + default: supertokens required: true pluginInterfaceBranch: description: 'supertokens-plugin-interface repos branch name' - default: master + default: master required: true jobs: diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 14ff0eb7..889d0053 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,18 +1,18 @@ - - - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6ed36dd3..3338eab6 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddf..9db25eef 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 247a91ec..1d781000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,124 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [7.1.0] - 2024-04-25 +## [7.3.0] -- Adds queries for Bulk Import +- Adds tables and queries for Bulk Import + +### Migration + +```sql +"CREATE TABLE IF NOT EXISTS bulk_import_users ( + id CHAR(36), + app_id VARCHAR(64) NOT NULL DEFAULT 'public', + primary_user_id VARCHAR(36), + raw_data TEXT NOT NULL, + status VARCHAR(128) DEFAULT 'NEW', + error_msg TEXT, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + CONSTRAINT bulk_import_users_pkey PRIMARY KEY(app_id, id), + CONSTRAINT bulk_import_users__app_id_fkey FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS bulk_import_users_status_updated_at_index ON bulk_import_users (app_id, status, updated_at); + +CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index1 ON bulk_import_users (app_id, status, created_at DESC, + id DESC); + +CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index2 ON bulk_import_users (app_id, created_at DESC, id DESC); +``` +## [7.2.0] - 2024-10-03 + +- Compatible with plugin interface version 6.3 +- Adds support for OAuthStorage + +### Migration + +```sql +CREATE TABLE IF NOT EXISTS oauth_clients ( + app_id VARCHAR(64), + client_id VARCHAR(255) NOT NULL, + is_client_credentials_only BOOLEAN NOT NULL, + PRIMARY KEY (app_id, client_id), + FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS oauth_sessions ( + gid VARCHAR(255), + app_id VARCHAR(64) DEFAULT 'public', + client_id VARCHAR(255) NOT NULL, + session_handle VARCHAR(128), + external_refresh_token VARCHAR(255) UNIQUE, + internal_refresh_token VARCHAR(255) UNIQUE, + jti TEXT NOT NULL, + exp BIGINT NOT NULL, + PRIMARY KEY (gid), + FOREIGN KEY(app_id, client_id) REFERENCES oauth_clients(app_id, client_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS oauth_session_exp_index ON oauth_sessions(exp DESC); +CREATE INDEX IF NOT EXISTS oauth_session_external_refresh_token_index ON oauth_sessions(app_id, external_refresh_token DESC); + +CREATE TABLE IF NOT EXISTS oauth_m2m_tokens ( + app_id VARCHAR(64) DEFAULT 'public', + client_id VARCHAR(255) NOT NULL, + iat BIGINT NOT NULL, + exp BIGINT NOT NULL, + PRIMARY KEY (app_id, client_id, iat), + FOREIGN KEY(app_id, client_id) REFERENCES oauth_clients(app_id, client_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS oauth_m2m_token_iat_index ON oauth_m2m_tokens(iat DESC, app_id DESC); +CREATE INDEX IF NOT EXISTS oauth_m2m_token_exp_index ON oauth_m2m_tokens(exp DESC); + +CREATE TABLE IF NOT EXISTS oauth_logout_challenges ( + app_id VARCHAR(64) DEFAULT 'public', + challenge VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, + post_logout_redirect_uri VARCHAR(1024), + session_handle VARCHAR(128), + state VARCHAR(128), + time_created BIGINT NOT NULL, + PRIMARY KEY (app_id, challenge), + FOREIGN KEY(app_id, client_id) REFERENCES oauth_clients(app_id, client_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS oauth_logout_challenges_time_created_index ON oauth_logout_challenges(time_created DESC); +``` + +## [7.1.3] - 2024-09-04 + +- Adds index on `last_active_time` for `user_last_active` table to improve the performance of MAU computation. + +### Migration + +```sql +CREATE INDEX IF NOT EXISTS user_last_active_last_active_time_index ON user_last_active (last_active_time DESC, app_id DESC); +``` + +## [7.1.2] - 2024-09-02 + +- Optimizes users count query + +## [7.1.1] - 2024-08-08 + +- Fixes tests that check for `Internal Error` in 500 status responses + +## [7.1.0] + +- Compatible with plugin interface version 6.2 +- Adds implementation for a new method `getConfigFieldsInfo` to fetch the plugin config fields. +- Adds `DashboardInfo` annotations to the config properties in `PostgreSQLConfig` +- Adds `null` state for `firstFactors` by adding `is_first_factors_null` field in `tenant_configs` table. The value of + this column is only applicable when there are no entries in the `tenant_first_factors` table for the tenant. + +### Migration + +```sql +ALTER TABLE tenant_configs ADD COLUMN IF NOT EXISTS is_first_factors_null BOOLEAN DEFAULT TRUE; +ALTER TABLE tenant_configs ALTER COLUMN is_first_factors_null DROP DEFAULT; +``` ## [7.0.1] - 2024-04-17 @@ -21,8 +136,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Support for MFA recipe - Adds `firstFactors` and `requiredSecondaryFactors` for tenant config. - Adds a new `useStaticKey` param to `updateSessionInfo_Transaction` - - This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to - change the signing key type of a session + - This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to + change the signing key type of a session ### Migration @@ -53,7 +168,8 @@ ALTER TABLE user_roles DROP CONSTRAINT IF EXISTS user_roles_role_fkey; - Fixes the issue where passwords were inadvertently logged in the logs. - Adds tests to check connection pool behaviour. -- Adds `postgresql_idle_connection_timeout` and `postgresql_minimum_idle_connections` configs to control active connections to the database. +- Adds `postgresql_idle_connection_timeout` and `postgresql_minimum_idle_connections` configs to control active + connections to the database. ## [5.0.6] - 2023-12-05 @@ -93,15 +209,16 @@ CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON app_id_to_ ### Changes - Support for Account Linking - - Adds columns `primary_or_recipe_user_id`, `is_linked_or_is_a_primary_user` and `primary_or_recipe_user_time_joined` to `all_auth_recipe_users` table - - Adds columns `primary_or_recipe_user_id` and `is_linked_or_is_a_primary_user` to `app_id_to_user_id` table - - Removes index `all_auth_recipe_users_pagination_index` and addes `all_auth_recipe_users_pagination_index1`, - `all_auth_recipe_users_pagination_index2`, `all_auth_recipe_users_pagination_index3` and - `all_auth_recipe_users_pagination_index4` indexes instead on `all_auth_recipe_users` table - - Adds `all_auth_recipe_users_recipe_id_index` on `all_auth_recipe_users` table - - Adds `all_auth_recipe_users_primary_user_id_index` on `all_auth_recipe_users` table - - Adds `email` column to `emailpassword_pswd_reset_tokens` table - - Changes `user_id` foreign key constraint on `emailpassword_pswd_reset_tokens` to `app_id_to_user_id` table + - Adds columns `primary_or_recipe_user_id`, `is_linked_or_is_a_primary_user` + and `primary_or_recipe_user_time_joined` to `all_auth_recipe_users` table + - Adds columns `primary_or_recipe_user_id` and `is_linked_or_is_a_primary_user` to `app_id_to_user_id` table + - Removes index `all_auth_recipe_users_pagination_index` and addes `all_auth_recipe_users_pagination_index1`, + `all_auth_recipe_users_pagination_index2`, `all_auth_recipe_users_pagination_index3` and + `all_auth_recipe_users_pagination_index4` indexes instead on `all_auth_recipe_users` table + - Adds `all_auth_recipe_users_recipe_id_index` on `all_auth_recipe_users` table + - Adds `all_auth_recipe_users_primary_user_id_index` on `all_auth_recipe_users` table + - Adds `email` column to `emailpassword_pswd_reset_tokens` table + - Changes `user_id` foreign key constraint on `emailpassword_pswd_reset_tokens` to `app_id_to_user_id` table ### Migration @@ -182,21 +299,20 @@ CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON app_id_to_ - Fixes null pointer issue when user belongs to no tenant. - ## [4.0.1] - 2023-07-11 - Fixes duplicate users in users search queries when user is associated to multiple tenants - ## [4.0.0] - 2023-06-02 ### Changes - Support for multitenancy - - New tables `apps` and `tenants` have been added. - - Schema of tables have been changed, adding `app_id` and `tenant_id` columns in tables and constraints & indexes have been modified to include this columns. - - New user tables have been added to map users to apps and tenants. - - New tables for multitenancy have been added. + - New tables `apps` and `tenants` have been added. + - Schema of tables have been changed, adding `app_id` and `tenant_id` columns in tables and constraints & indexes + have been modified to include this columns. + - New user tables have been added to map users to apps and tenants. + - New tables for multitenancy have been added. - Increased transaction retry count to 50 from 20. ### Migration @@ -1074,19 +1190,19 @@ CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON app_id_to_ ### Migration - If using `access_token_signing_key_dynamic` false in the core: - - ```sql - ALTER TABLE session_info ADD COLUMN use_static_key BOOLEAN NOT NULL DEFAULT(true); - ALTER TABLE session_info ALTER COLUMN use_static_key DROP DEFAULT; + - ```sql + ALTER TABLE session_info ADD COLUMN use_static_key BOOLEAN NOT NULL DEFAULT(true); + ALTER TABLE session_info ALTER COLUMN use_static_key DROP DEFAULT; ``` - - ```sql + - ```sql INSERT INTO jwt_signing_keys(key_id, key_string, algorithm, created_at) select CONCAT('s-', created_at_time) as key_id, value as key_string, 'RS256' as algorithm, created_at_time as created_at from session_access_token_signing_keys; ``` - If using `access_token_signing_key_dynamic` true (or not set) in the core: - - ```sql - ALTER TABLE session_info ADD COLUMN use_static_key BOOLEAN NOT NULL DEFAULT(false); - ALTER TABLE session_info ALTER COLUMN use_static_key DROP DEFAULT; + - ```sql + ALTER TABLE session_info ADD COLUMN use_static_key BOOLEAN NOT NULL DEFAULT(false); + ALTER TABLE session_info ALTER COLUMN use_static_key DROP DEFAULT; ``` ## [2.4.0] - 2023-03-30 @@ -1094,14 +1210,17 @@ CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON app_id_to_ - Support for Dashboard Search ## [2.3.0] - 2023-03-27 + - Support for TOTP recipe - Support for active users ### Database changes + - Add new tables for TOTP recipe: - - `totp_users` that stores the users that have enabled TOTP - - `totp_user_devices` that stores devices (each device has its own secret) for each user - - `totp_used_codes` that stores used codes for each user. This is to implement rate limiting and prevent replay attacks. + - `totp_users` that stores the users that have enabled TOTP + - `totp_user_devices` that stores devices (each device has its own secret) for each user + - `totp_used_codes` that stores used codes for each user. This is to implement rate limiting and prevent replay + attacks. - Add `user_last_active` table to store the last active time of a user. ## [2.2.0] - 2023-02-21 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53732985..a222aea4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,51 +1,70 @@ - # Contributing -We're so excited you're interested in helping with SuperTokens! We are happy to help you get started, even if you don't have any previous open-source experience :blush: +We're so excited you're interested in helping with SuperTokens! We are happy to help you get started, even if you don't +have any previous open-source experience :blush: ## New to Open Source? -1. Take a look at [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) -2. Go thorugh the [SuperTokens Code of Conduct](https://github.com/supertokens/supertokens-postgresql-plugin/blob/master/CODE_OF_CONDUCT.md) + +1. Take a look + at [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) +2. Go thorugh + the [SuperTokens Code of Conduct](https://github.com/supertokens/supertokens-postgresql-plugin/blob/master/CODE_OF_CONDUCT.md) ## Where to ask Questions? -1. Check our [Github Issues](https://github.com/supertokens/supertokens-postgresql-plugin/issues) to see if someone has already answered your question. -2. Join our community on [Discord](https://supertokens.io/discord) and feel free to ask us your questions +1. Check our [Github Issues](https://github.com/supertokens/supertokens-postgresql-plugin/issues) to see if someone has + already answered your question. +2. Join our community on [Discord](https://supertokens.io/discord) and feel free to ask us your questions ## Development Setup + ### Prerequisites + - OS: Linux or macOS - IDE: Intellij (recommended) or equivalent IDE - PostgreSQL ### Project Setup -1. Setup the `supertokens-core` by following [this guide](https://github.com/supertokens/supertokens-core/blob/master/CONTRIBUTING.md#development-setup). If you are not modifying the `supertokens-core` repo, then you do not need to fork that. + +1. Setup the `supertokens-core` by + following [this guide](https://github.com/supertokens/supertokens-core/blob/master/CONTRIBUTING.md#development-setup). + If you are not modifying the `supertokens-core` repo, then you do not need to fork that. 2. Start PostgreSQL on port `5432`, listening to `locahost` or `0.0.0.0`. 3. Create a PostgreSQL user (if not already exists) with username `root` and password `root` 4. Create a database called `supertokens`. 5. Fork the `supertokens-pstgresql-plugin` repository -6. Open `modules.txt` in the `supertokens-root` directory and change it so that it looks like (the last line has changed): +6. Open `modules.txt` in the `supertokens-root` directory and change it so that it looks like (the last line has + changed): ``` // put module name like module name,branch name,github username(if contributing with a forked repository) and then call ./loadModules script core,master plugin-interface,master postgresql-plugin,master, ``` -7. Run `./loadModules` in the `supertokens-root` directory. This will clone your forked `supertokens-postgresql-plugin` repo. -8. Follow the [CONTRIBUTING.md](https://github.com/supertokens/supertokens-core/blob/master/CONTRIBUTING.md#modifying-code) guide from `supertokens-core` repo for modifying and testing. +7. Run `./loadModules` in the `supertokens-root` directory. This will clone your forked `supertokens-postgresql-plugin` + repo. +8. Follow + the [CONTRIBUTING.md](https://github.com/supertokens/supertokens-core/blob/master/CONTRIBUTING.md#modifying-code) + guide from `supertokens-core` repo for modifying and testing. ## Pull Request + 1. Before submitting a pull request make sure all tests have passed -2. Reference the relevant issue or pull request and give a clear description of changes/features added when submitting a pull request +2. Reference the relevant issue or pull request and give a clear description of changes/features added when submitting a + pull request 3. Make sure the PR title follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification ## SuperTokens Community -SuperTokens is made possible by a passionate team and a strong community of developers. If you have any questions or would like to get more involved in the SuperTokens community you can check out: - - [Github Issues](https://github.com/supertokens/supertokens-postgresql-plugin/issues) - - [Discord](https://supertokens.io/discord) - - [Twitter](https://twitter.com/supertokensio) - - or [email us](mailto:team@supertokens.io) - + +SuperTokens is made possible by a passionate team and a strong community of developers. If you have any questions or +would like to get more involved in the SuperTokens community you can check out: + +- [Github Issues](https://github.com/supertokens/supertokens-postgresql-plugin/issues) +- [Discord](https://supertokens.io/discord) +- [Twitter](https://twitter.com/supertokensio) +- or [email us](mailto:team@supertokens.io) + Additional resources you might find useful: - - [SuperTokens Docs](https://supertokens.io/docs/community/getting-started/installation) - - [Blog Posts](https://supertokens.io/blog/) + +- [SuperTokens Docs](https://supertokens.io/docs/community/getting-started/installation) +- [Blog Posts](https://supertokens.io/blog/) diff --git a/LICENSE.md b/LICENSE.md index e105852e..7a85c9c7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,191 +1,192 @@ - Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. - - This software is licensed under the Apache License, Version 2.0 (the - "License") as published by the Apache Software Foundation. - - You may not use this software except in compliance with the License. A copy - of the License is available below the line. - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - License for the specific language governing permissions and limitations - under the License. +Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. + +This software is licensed under the Apache License, Version 2.0 (the +"License") as published by the Apache Software Foundation. + +You may not use this software except in compliance with the License. A copy +of the License is available below the line. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. ------------------------------------------------------------------------------- + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index cc88cb0b..d44d464f 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,32 @@ - ![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) # PostgreSQL plugin for SuperTokens Community + chat on Discord ## About + This plugin is responsible for interfacing between SuperTokens Community version and an instance of PostgreSQL. Learn more at https://supertokens.io ## Documentation + To see documentation, please click [here](https://supertokens.io/docs/community/introduction). ## Contributing -Please refer to the [CONTRIBUTING.md](https://github.com/supertokens/supertokens-postgresql-plugin/blob/master/CONTRIBUTING.md) file in this repo. + +Please refer to +the [CONTRIBUTING.md](https://github.com/supertokens/supertokens-postgresql-plugin/blob/master/CONTRIBUTING.md) file in +this repo. ## Contact us -For any queries, or support requests, please email us at team@supertokens.io, or join our [Discord](supertokens.io/discord) server. + +For any queries, or support requests, please email us at team@supertokens.io, or join +our [Discord](supertokens.io/discord) server. # Authors + Created with :heart: by the folks at SuperTokens.io. diff --git a/build.gradle b/build.gradle index fdcd09f6..112bd9a1 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "7.1.0" +version = "7.3.0" repositories { mavenCentral() diff --git a/config.yaml b/config.yaml index 38ade78f..f42b0851 100644 --- a/config.yaml +++ b/config.yaml @@ -22,7 +22,7 @@ postgresql_config_version: 0 # (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. The PostgreSQL user to use to query the database. # If the relevant tables are not already created by you, this user should have the -# ability to create new tables. To see the tables needed, visit: https://supertokens.io/docs/community/getting-started/database-setup/postgresql +# ability to create new tables. To see the tables needed, visit: https://supertokens.com/docs/thirdpartyemailpassword/pre-built-ui/setup/database-setup/postgresql # postgresql_user: # (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. Password for the PostgreSQL user. If you have not set a password @@ -37,7 +37,7 @@ postgresql_config_version: 0 # postgresql_table_schema: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "") string value. A prefix to add to all table names managed by -# SuperTokens. An "_" will be added between this prefix and the actual table name if the prefix is defined +# SuperTokens. An "_" will be added between this prefix and the actual table name if the prefix is defined. # postgresql_table_names_prefix: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "key_value") string value. Specify the name of the table that will @@ -66,7 +66,7 @@ postgresql_config_version: 0 # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table # that will store the thirdparty recipe users. -# postgresql_thirdparty_users_table_name +# postgresql_thirdparty_users_table_name: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections # to be closed. diff --git a/devConfig.yaml b/devConfig.yaml index a25dba97..09eb3e55 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -23,7 +23,7 @@ postgresql_config_version: 0 # (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. The PostgreSQL user to use to query the database. # If the relevant tables are not already created by you, this user should have the -# ability to create new tables. To see the tables needed, visit: TODO +# ability to create new tables. To see the tables needed, visit: https://supertokens.com/docs/thirdpartyemailpassword/pre-built-ui/setup/database-setup/postgresql postgresql_user: "root" # (DIFFERENT_ACROSS_TENANTS | COMPULSORY) string value. Password for the PostgreSQL instance. If you do not have a @@ -39,7 +39,7 @@ postgresql_password: "root" # postgresql_table_schema: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "") string value. A prefix to add to all table names managed by -# SuperTokens. An "_" will be added between this prefix and the actual table name if the prefix is defined +# SuperTokens. An "_" will be added between this prefix and the actual table name if the prefix is defined. # postgresql_table_names_prefix: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "key_value") string value. Specify the name of the table that will @@ -68,7 +68,7 @@ postgresql_password: "root" # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table # that will store the thirdparty recipe users. -# postgresql_thirdparty_users_table_name +# postgresql_thirdparty_users_table_name: # (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 60000) long value. Timeout in milliseconds for the idle connections # to be closed. diff --git a/jar/postgresql-plugin-7.0.1.jar b/jar/postgresql-plugin-7.0.1.jar deleted file mode 100644 index 7437a722..00000000 Binary files a/jar/postgresql-plugin-7.0.1.jar and /dev/null differ diff --git a/jar/postgresql-plugin-7.2.0.jar b/jar/postgresql-plugin-7.2.0.jar new file mode 100644 index 00000000..d8e15676 Binary files /dev/null and b/jar/postgresql-plugin-7.2.0.jar differ diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index 476e2b85..25f82381 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "6.1" + "6.3" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyConnection.java b/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyConnection.java index b25df932..382a8f2e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyConnection.java +++ b/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyConnection.java @@ -16,21 +16,7 @@ package io.supertokens.storage.postgresql; -import java.sql.Array; -import java.sql.Blob; -import java.sql.CallableStatement; -import java.sql.Clob; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.NClob; -import java.sql.PreparedStatement; -import java.sql.SQLClientInfoException; -import java.sql.SQLException; -import java.sql.SQLWarning; -import java.sql.SQLXML; -import java.sql.Savepoint; -import java.sql.Statement; -import java.sql.Struct; +import java.sql.*; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; @@ -53,17 +39,17 @@ public BulkImportProxyConnection(Connection con) { @Override public void close() throws SQLException { - // We simply ignore when close is called BulkImportProxyConnection + //this.con.close(); // why are we against the close? } @Override public void commit() throws SQLException { - // We simply ignore when commit is called BulkImportProxyConnection + //this.con.commit(); } @Override public void rollback() throws SQLException { - // We simply ignore when rollback is called BulkImportProxyConnection + //this.con.rollback(); } public void closeForBulkImportProxyStorage() throws SQLException { diff --git a/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyStorage.java b/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyStorage.java index f4e8dd8e..12eeff8a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyStorage.java +++ b/src/main/java/io/supertokens/storage/postgresql/BulkImportProxyStorage.java @@ -16,21 +16,17 @@ package io.supertokens.storage.postgresql; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.List; -import java.util.Set; - -import com.google.gson.JsonObject; - -import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.exceptions.DbInitException; -import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + /** * BulkImportProxyStorage is a class extending Start, serving as a Storage instance in the bulk import user cronjob. @@ -48,7 +44,7 @@ public synchronized Connection getTransactionConnection() throws SQLException, S if (this.connection == null) { Connection con = ConnectionPool.getConnectionForProxyStorage(this); this.connection = new BulkImportProxyConnection(con); - connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); connection.setAutoCommit(false); } return this.connection; @@ -56,23 +52,15 @@ public synchronized Connection getTransactionConnection() throws SQLException, S @Override protected T startTransactionHelper(TransactionLogic logic, TransactionIsolationLevel isolationLevel) - throws StorageQueryException, StorageTransactionLogicException, SQLException { + throws StorageQueryException, StorageTransactionLogicException, SQLException, TenantOrAppNotFoundException { return logic.mainLogicAndCommit(new TransactionConnection(getTransactionConnection())); } @Override public void commitTransaction(TransactionConnection con) throws StorageQueryException { - // We do not want to commit the queries when using the BulkImportProxyStorage to be able to rollback everything + // We do not want to commit the queries when using the BulkImportProxyStorage to be able to rollback everything // if any query fails while importing the user - } - - @Override - public void loadConfig(JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) - throws InvalidConfigException { - // We are overriding the loadConfig method to set the connection pool size - // to 1 to avoid creating many connections for the bulk import cronjob - configJson.addProperty("postgresql_connection_pool_size", 1); - super.loadConfig(configJson, logLevels, tenantIdentifier); + //super.commitTransaction(con); } @Override @@ -95,7 +83,7 @@ public void initStorage(boolean shouldWait, List tenantIdentif public void closeConnectionForBulkImportProxyStorage() throws StorageQueryException { try { if (this.connection != null) { - this.connection.close(); + this.connection.closeForBulkImportProxyStorage(); this.connection = null; } ConnectionPool.close(this); diff --git a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java index 62aa8d2a..70af1f53 100644 --- a/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/postgresql/ConnectionPool.java @@ -149,7 +149,8 @@ static boolean isAlreadyInitialised(Start start) { return getInstance(start) != null && getInstance(start).hikariDataSource != null; } - static void initPool(Start start, boolean shouldWait, PostConnectCallback postConnectCallback) throws DbInitException { + static void initPool(Start start, boolean shouldWait, PostConnectCallback postConnectCallback) + throws DbInitException { if (isAlreadyInitialised(start)) { return; } diff --git a/src/main/java/io/supertokens/storage/postgresql/QueryExecutorTemplate.java b/src/main/java/io/supertokens/storage/postgresql/QueryExecutorTemplate.java index 4fed4abc..9b408b5e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/QueryExecutorTemplate.java +++ b/src/main/java/io/supertokens/storage/postgresql/QueryExecutorTemplate.java @@ -26,14 +26,14 @@ public interface QueryExecutorTemplate { static T execute(Start start, String QUERY, PreparedStatementValueSetter setter, - ResultSetValueExtractor mapper) throws SQLException, StorageQueryException { + ResultSetValueExtractor mapper) throws SQLException, StorageQueryException { try (Connection con = ConnectionPool.getConnection(start)) { return execute(con, QUERY, setter, mapper); } } static T execute(Connection con, String QUERY, PreparedStatementValueSetter setter, - ResultSetValueExtractor mapper) throws SQLException, StorageQueryException { + ResultSetValueExtractor mapper) throws SQLException, StorageQueryException { if (setter == null) setter = PreparedStatementValueSetter.NO_OP_SETTER; try (PreparedStatement pst = con.prepareStatement(QUERY)) { diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 411e52b3..7d12083e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -25,14 +25,17 @@ import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; -import io.supertokens.pluginInterface.bulkimport.BulkImportStorage.BULK_IMPORT_USER_STATUS; -import io.supertokens.pluginInterface.bulkimport.sqlStorage.BulkImportSQLStorage; +import io.supertokens.pluginInterface.bulkimport.BulkImportStorage; import io.supertokens.pluginInterface.bulkimport.BulkImportUser; +import io.supertokens.pluginInterface.bulkimport.exceptions.BulkImportBatchInsertException; +import io.supertokens.pluginInterface.bulkimport.exceptions.BulkImportTransactionRolledBackException; +import io.supertokens.pluginInterface.bulkimport.sqlStorage.BulkImportSQLStorage; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.dashboard.exceptions.UserIdNotFoundException; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; +import io.supertokens.pluginInterface.emailpassword.EmailPasswordImportUser; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException; @@ -51,20 +54,30 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; -import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; +import io.supertokens.pluginInterface.oauth.OAuthClient; +import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; +import io.supertokens.pluginInterface.oauth.OAuthStorage; +import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException; +import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; +import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser; import io.supertokens.pluginInterface.passwordless.exception.*; import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; import io.supertokens.pluginInterface.session.SessionInfo; import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; +import io.supertokens.pluginInterface.thirdparty.ThirdPartyImportUser; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; import io.supertokens.pluginInterface.totp.TOTPDevice; @@ -98,6 +111,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLTransactionRollbackException; @@ -109,20 +123,21 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, - ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage, BulkImportSQLStorage { + ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage, OAuthStorage, BulkImportSQLStorage { // these configs are protected from being modified / viewed by the dev using the SuperTokens // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. private static String[] PROTECTED_DB_CONFIG = new String[]{"postgresql_connection_pool_size", "postgresql_connection_uri", "postgresql_host", "postgresql_port", "postgresql_user", "postgresql_password", "postgresql_database_name", "postgresql_table_schema", "postgresql_idle_connection_timeout", - "postgresql_minimum_idle_connections"}; + "postgresql_minimum_idle_connections", "postgresql_connection_attributes", "postgresql_connection_scheme", + "postgresql_table_names_prefix" + }; private static final Object appenderLock = new Object(); public static boolean silent = false; private ResourceDistributor resourceDistributor = new ResourceDistributor(); private String processId; private HikariLoggingAppender appender; - private static final String APP_ID_KEY_NAME = "app_id"; private static final String ACCESS_TOKEN_SIGNING_KEY_NAME = "access_token_signing_key"; private static final String REFRESH_TOKEN_KEY_NAME = "refresh_token_key"; public static boolean isTesting = false; @@ -285,7 +300,8 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev tries++; try { return startTransactionHelper(logic, isolationLevel); - } catch (SQLException | StorageQueryException | StorageTransactionLogicException e) { + } catch (SQLException | StorageQueryException | StorageTransactionLogicException | + TenantOrAppNotFoundException e) { Throwable actualException = e; if (e instanceof StorageQueryException) { actualException = e.getCause(); @@ -322,6 +338,13 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev if ((isPSQLRollbackException || isDeadlockException) && tries < NUM_TRIES) { try { + if(this instanceof BulkImportProxyStorage){ + throw new StorageTransactionLogicException(new BulkImportTransactionRolledBackException(e)); + // if the current instance is of BulkImportProxyStorage, that means we are doing a bulk import + // which uses nested transactions. With MySQL this retry logic doesn't going to work, we have + // to retry the whole "big" transaction, not just the innermost, current one. + // @see BulkImportTransactionRolledBackException for more explanation. + } Thread.sleep((long) (10 + (250 + Math.min(Math.pow(2, tries), 3000)) * Math.random())); } catch (InterruptedException ignored) { } @@ -338,6 +361,8 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev throw (StorageQueryException) e; } else if (e instanceof StorageTransactionLogicException) { throw (StorageTransactionLogicException) e; + } else if (e instanceof TenantOrAppNotFoundException) { + throw new StorageTransactionLogicException(e); } throw new StorageQueryException(e); } @@ -345,7 +370,7 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev } protected T startTransactionHelper(TransactionLogic logic, TransactionIsolationLevel isolationLevel) - throws StorageQueryException, StorageTransactionLogicException, SQLException { + throws StorageQueryException, StorageTransactionLogicException, SQLException, TenantOrAppNotFoundException { Connection con = null; Integer defaultTransactionIsolation = null; try { @@ -785,13 +810,6 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str } catch (SQLException e) { throw new StorageQueryException(e); } - } else if (className.equals(TOTPStorage.class.getName())) { - try { - TOTPDevice[] devices = TOTPQueries.getDevices(this, appIdentifier, userId); - return devices.length > 0; - } catch (SQLException e) { - throw new StorageQueryException(e); - } } else if (className.equals(JWTRecipeStorage.class.getName())) { return false; } else if (className.equals(ActiveUsersStorage.class.getName())) { @@ -801,6 +819,73 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str } } + @Override + public Map> findNonAuthRecipesWhereForUserIdsUsed(AppIdentifier appIdentifier, + List userIds) + throws StorageQueryException { + try { + Map> sessionHandlesByUserId = SessionQueries.getAllNonExpiredSessionHandlesForUsers(this, appIdentifier, userIds); + Map> rolesByUserIds = UserRolesQueries.getRolesForUsers(this, appIdentifier, userIds); + Map userMetadatasByIds = UserMetadataQueries.getMultipleUserMetadatas(this, appIdentifier, userIds); + Set userIdsUsedInEmailVerification = EmailVerificationQueries.findUserIdsBeingUsedForEmailVerification(this, appIdentifier, userIds); + Map> devicesByUserIds = TOTPQueries.getDevicesForMultipleUsers(this, appIdentifier, userIds); + Map lastActivesByUserIds = ActiveUsersQueries.getLastActiveByMultipleUserIds(this, appIdentifier, userIds); + + Map> nonAuthRecipeClassnamesByUserIds = new HashMap<>(); + //session recipe + for(String userId: sessionHandlesByUserId.keySet()){ + if(!nonAuthRecipeClassnamesByUserIds.containsKey(userId)){ + nonAuthRecipeClassnamesByUserIds.put(userId, new ArrayList<>()); + } + nonAuthRecipeClassnamesByUserIds.get(userId).add(SessionStorage.class.getName()); + } + + //role recipe + for(String userId: rolesByUserIds.keySet()){ + if(!nonAuthRecipeClassnamesByUserIds.containsKey(userId)){ + nonAuthRecipeClassnamesByUserIds.put(userId, new ArrayList<>()); + } + nonAuthRecipeClassnamesByUserIds.get(userId).add(UserRolesStorage.class.getName()); + } + + //usermetadata recipe + for(String userId: userMetadatasByIds.keySet()){ + if(!nonAuthRecipeClassnamesByUserIds.containsKey(userId)){ + nonAuthRecipeClassnamesByUserIds.put(userId, new ArrayList<>()); + } + nonAuthRecipeClassnamesByUserIds.get(userId).add(UserMetadataStorage.class.getName()); + } + + //emailverification recipe + for(String userId: userIdsUsedInEmailVerification){ + if(!nonAuthRecipeClassnamesByUserIds.containsKey(userId)){ + nonAuthRecipeClassnamesByUserIds.put(userId, new ArrayList<>()); + } + nonAuthRecipeClassnamesByUserIds.get(userId).add(EmailVerificationStorage.class.getName()); + } + + //totp recipe + for(String userId: devicesByUserIds.keySet()){ + if(!nonAuthRecipeClassnamesByUserIds.containsKey(userId)){ + nonAuthRecipeClassnamesByUserIds.put(userId, new ArrayList<>()); + } + nonAuthRecipeClassnamesByUserIds.get(userId).add(TOTPStorage.class.getName()); + } + + //active users + for(String userId: lastActivesByUserIds.keySet()){ + if(!nonAuthRecipeClassnamesByUserIds.containsKey(userId)){ + nonAuthRecipeClassnamesByUserIds.put(userId, new ArrayList<>()); + } + nonAuthRecipeClassnamesByUserIds.get(userId).add(ActiveUsersStorage.class.getName()); + } + + return nonAuthRecipeClassnamesByUserIds; + } catch (SQLException | StorageTransactionLogicException exc) { + throw new StorageQueryException(exc); + } + } + @TestOnly @Override public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) @@ -867,7 +952,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } } else if (className.equals(TOTPStorage.class.getName())) { try { - TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false, System.currentTimeMillis()); + TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false, + System.currentTimeMillis()); this.startTransaction(con -> { try { long now = System.currentTimeMillis(); @@ -887,6 +973,11 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } } else if (className.equals(JWTRecipeStorage.class.getName())) { /* Since JWT recipe tables do not store userId we do not add any data to them */ + + } else if (className.equals(BulkImportStorage.class.getName())){ + //ignore + } else if (className.equals(OAuthStorage.class.getName())) { + /* Since OAuth recipe tables do not store userId we do not add any data to them */ } else if (className.equals(ActiveUsersStorage.class.getName())) { try { ActiveUsersQueries.updateUserLastActive(this, tenantIdentifier.toAppIdentifier(), userId); @@ -910,7 +1001,7 @@ public String[] getProtectedConfigsFromSuperTokensSaaSUsers() { @Override public AuthRecipeUserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, - long timeJoined) + long timeJoined) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, TenantOrAppNotFoundException { try { @@ -942,6 +1033,56 @@ public AuthRecipeUserInfo signUp(TenantIdentifier tenantIdentifier, String id, S } } + @Override + public void signUpMultipleViaBulkImport_Transaction(TransactionConnection connection, List users) + throws StorageQueryException, StorageTransactionLogicException { + try { + Connection sqlConnection = (Connection) connection.getConnection(); + EmailPasswordQueries.signUpMultipleForBulkImport_Transaction(this, sqlConnection, users); + } catch (StorageQueryException | SQLException | StorageTransactionLogicException e) { + Throwable actual = e.getCause(); + if (actual instanceof BatchUpdateException) { + BatchUpdateException batchUpdateException = (BatchUpdateException) actual; + Map errorByPosition = new HashMap<>(); + SQLException nextException = batchUpdateException.getNextException(); + while (nextException != null) { + + if (nextException instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) nextException).getServerErrorMessage(); + + int position = getErroneousEntryPosition(batchUpdateException); + if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(), + "email")) { + + errorByPosition.put(users.get(position).userId, new DuplicateEmailException()); + + } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable()) + || isPrimaryKeyError(serverMessage, config.getUsersTable()) + || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable()) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { + errorByPosition.put(users.get(position).userId, + new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException()); + } + + } + nextException = nextException.getNextException(); + } + throw new StorageTransactionLogicException(new BulkImportBatchInsertException("emailpassword errors", errorByPosition)); + } + throw new StorageQueryException(e); + } + } + + private static int getErroneousEntryPosition(BatchUpdateException batchUpdateException) { + String errorMessage = batchUpdateException.getMessage(); + String searchFor = "Batch entry "; + int searchForIndex = errorMessage.indexOf("Batch entry "); + String entryIndex = errorMessage.substring(searchForIndex + searchFor.length(), errorMessage.indexOf(" ", searchForIndex + searchFor.length())); + int position = Integer.parseInt(entryIndex); + return position; + } + @Override public void deleteEmailPasswordUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, boolean deleteUserIdMappingToo) @@ -1119,6 +1260,35 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans } } + @Override + public void updateMultipleIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + Map emailToUserId, boolean isEmailVerified) + throws StorageQueryException, TenantOrAppNotFoundException { + Connection sqlCon = (Connection) con.getConnection(); + try { + EmailVerificationQueries.updateMultipleUsersIsEmailVerified_Transaction(this, sqlCon, appIdentifier, + emailToUserId, isEmailVerified); + } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } + + boolean isPSQLPrimKeyError = e instanceof PSQLException && isPrimaryKeyError( + ((PSQLException) e).getServerErrorMessage(), + Config.getConfig(this).getEmailVerificationTable()); + + if (!isEmailVerified || !isPSQLPrimKeyError) { + throw new StorageQueryException(e); + } + // we do not throw an error since the email is already verified + } + } + @Override public void deleteEmailVerificationUserInfo_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) @@ -1220,8 +1390,17 @@ public boolean isEmailVerified(AppIdentifier appIdentifier, String userId, Strin } @Override - public void updateIsEmailVerifiedToExternalUserId(AppIdentifier appIdentifier, String supertokensUserId, String externalUserId) throws StorageQueryException { - EmailVerificationQueries.updateIsEmailVerifiedToExternalUserId(this, appIdentifier, supertokensUserId, externalUserId); + public void updateIsEmailVerifiedToExternalUserId(AppIdentifier appIdentifier, String supertokensUserId, + String externalUserId) throws StorageQueryException { + EmailVerificationQueries.updateIsEmailVerifiedToExternalUserId(this, appIdentifier, supertokensUserId, + externalUserId); + } + + @Override + public void updateMultipleIsEmailVerifiedToExternalUserIds(AppIdentifier appIdentifier, + Map supertokensUserIdToExternalUserId) + throws StorageQueryException { + EmailVerificationQueries.updateMultipleIsEmailVerifiedToExternalUserIds(this, appIdentifier, supertokensUserIdToExternalUserId); } @Override @@ -1301,6 +1480,53 @@ public void deleteThirdPartyUser_Transaction(TransactionConnection con, AppIdent } } + @Override + public void importThirdPartyUsers_Transaction(TransactionConnection con, + List usersToImport) + throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException { + try { + Connection sqlCon = (Connection) con.getConnection(); + ThirdPartyQueries.importUser_Transaction(this, sqlCon, usersToImport); + } catch (SQLException e) { + Throwable actual = e.getCause(); + if (actual instanceof BatchUpdateException) { + BatchUpdateException batchUpdateException = (BatchUpdateException) actual; + Map errorByPosition = new HashMap<>(); + SQLException nextException = batchUpdateException.getNextException(); + while (nextException != null) { + + if (nextException instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) nextException).getServerErrorMessage(); + + int position = getErroneousEntryPosition(batchUpdateException); + if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(), + "third_party_user_id")) { + + errorByPosition.put(usersToImport.get(position).userId, new DuplicateThirdPartyUserException()); + + } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable()) + || isPrimaryKeyError(serverMessage, config.getUsersTable()) + || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable()) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { + errorByPosition.put(usersToImport.get(position).userId, + new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException()); + } + else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), "app_id")) { + throw new TenantOrAppNotFoundException(usersToImport.get(position).tenantIdentifier.toAppIdentifier()); + + } else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), "tenant_id")) { + throw new TenantOrAppNotFoundException(usersToImport.get(position).tenantIdentifier); + } + } + nextException = nextException.getNextException(); + } + throw new StorageTransactionLogicException(new BulkImportBatchInsertException("thirdparty errors", errorByPosition)); + } + throw new StorageQueryException(e); + } + } + @Override public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) throws StorageQueryException { @@ -1353,6 +1579,15 @@ public void updateLastActive(AppIdentifier appIdentifier, String userId) throws } } + @TestOnly + public void updateLastActive(AppIdentifier appIdentifier, String userId, long timestamp) throws StorageQueryException { + try { + ActiveUsersQueries.updateUserLastActive(this, appIdentifier, userId, timestamp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { @@ -1383,6 +1618,16 @@ public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) } } + @Override + public List findExistingUserIds(AppIdentifier appIdentifier, List userIds) + throws StorageQueryException { + try { + return GeneralQueries.findUserIdsThatExist(this, appIdentifier, userIds); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public AuthRecipeUserInfo getPrimaryUserById(AppIdentifier appIdentifier, String userId) throws StorageQueryException { @@ -1742,10 +1987,10 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) @Override public AuthRecipeUserInfo createUser(TenantIdentifier tenantIdentifier, - String id, - @javax.annotation.Nullable String email, - @javax.annotation.Nullable - String phoneNumber, long timeJoined) + String id, + @javax.annotation.Nullable String email, + @javax.annotation.Nullable + String phoneNumber, long timeJoined) throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException, TenantOrAppNotFoundException { @@ -1810,6 +2055,61 @@ public void deletePasswordlessUser_Transaction(TransactionConnection con, AppIde } } + @Override + public void importPasswordlessUsers_Transaction(TransactionConnection con, + List users) + throws StorageQueryException, TenantOrAppNotFoundException { + try { + Connection sqlCon = (Connection) con.getConnection(); + PasswordlessQueries.importUsers_Transaction(sqlCon, this, users); + } catch (SQLException e) { + if (e instanceof BatchUpdateException) { + Throwable actual = e.getCause(); + if (actual instanceof BatchUpdateException) { + BatchUpdateException batchUpdateException = (BatchUpdateException) actual; + Map errorByPosition = new HashMap<>(); + SQLException nextException = batchUpdateException.getNextException(); + while (nextException != null) { + + if (nextException instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) nextException).getServerErrorMessage(); + + int position = getErroneousEntryPosition(batchUpdateException); + + if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable()) + || isPrimaryKeyError(serverMessage, config.getUsersTable()) + || isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable()) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) { + errorByPosition.put(users.get(position).userId, new DuplicateUserIdException()); + } + if (isUniqueConstraintError(serverMessage, config.getPasswordlessUserToTenantTable(), + "email")) { + errorByPosition.put(users.get(position).userId, new DuplicateEmailException()); + + } else if (isUniqueConstraintError(serverMessage, config.getPasswordlessUserToTenantTable(), + "phone_number")) { + errorByPosition.put(users.get(position).userId, new DuplicatePhoneNumberException()); + + } else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(), + "app_id")) { + throw new TenantOrAppNotFoundException(users.get(position).tenantIdentifier.toAppIdentifier()); + + } else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), + "tenant_id")) { + throw new TenantOrAppNotFoundException(users.get(position).tenantIdentifier.toAppIdentifier()); + } + } + nextException = nextException.getNextException(); + } + throw new StorageQueryException( + new BulkImportBatchInsertException("passwordless errors", errorByPosition)); + } + throw new StorageQueryException(e); + } + } + } + @Override public PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException { @@ -1902,6 +2202,18 @@ public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, Trans } } + @Override + public Map getMultipleUsersMetadatas_Transaction(AppIdentifier appIdentifier, TransactionConnection + con, List userIds) + throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + return UserMetadataQueries.getMultipleUsersMetadatas_Transaction(this, sqlCon, appIdentifier, userIds); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, JsonObject metadata) @@ -1923,6 +2235,26 @@ public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionC } } + @Override + public void setMultipleUsersMetadatas_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + Map metadataByUserId) + throws StorageQueryException, TenantOrAppNotFoundException { + Connection sqlCon = (Connection) con.getConnection(); + try { + UserMetadataQueries.setMultipleUsersMetadatas_Transaction(this, sqlCon, appIdentifier, metadataByUserId); + } catch (SQLException e) { + if (e instanceof PSQLException) { + PostgreSQLConfig config = Config.getConfig(this); + ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage(); + + if (isForeignKeyConstraintError(serverMessage, config.getUserMetadataTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } + throw new StorageQueryException(e); + } + } + @Override public int deleteUserMetadata_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { @@ -1963,6 +2295,17 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri } throw new StorageQueryException(e); } + } + + @Override + public void addRolesToUsers_Transaction(TransactionConnection connection, + Map>> rolesToUserByTenants) + throws StorageQueryException { + try { + UserRolesQueries.addRolesToUsers_Transaction(this, (Connection) connection.getConnection(), rolesToUserByTenants); + } catch (SQLException e) { + throw new StorageQueryException(e); + } } @@ -2170,6 +2513,17 @@ public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, Transactio } } + @Override + public List doesMultipleRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + List roles) throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + return UserRolesQueries.doesMultipleRoleExist_transaction(this, sqlCon, appIdentifier, roles); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, @org.jetbrains.annotations.Nullable String externalUserIdInfo) @@ -2205,6 +2559,19 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU } + @Override + public void createBulkUserIdMapping(AppIdentifier appIdentifier, + Map superTokensUserIdToExternalUserId) + throws StorageQueryException { + try { + + UserIdMappingQueries.createBulkUserIdMapping(this, appIdentifier, superTokensUserIdToExternalUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + + } + @Override public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { @@ -2266,7 +2633,8 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str } @Override - public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, ArrayList userIds) + public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, + ArrayList userIds) throws StorageQueryException { try { return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds); @@ -2385,7 +2753,8 @@ public TenantConfig[] getAllTenants() throws StorageQueryException { } @Override - public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId) + public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String userId) throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { Connection sqlCon = (Connection) con.getConnection(); @@ -2684,7 +3053,8 @@ public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) } @Override - public TOTPDevice createDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, TOTPDevice device) + public TOTPDevice createDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + TOTPDevice device) throws StorageQueryException, DeviceAlreadyExistsException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -2697,9 +3067,9 @@ public TOTPDevice createDevice_Transaction(TransactionConnection con, AppIdentif ServerErrorMessage errMsg = ((PSQLException) actualException).getServerErrorMessage(); if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { - throw new DeviceAlreadyExistsException(); + throw new DeviceAlreadyExistsException(); } else if (isForeignKeyConstraintError(errMsg, Config.getConfig(this).getTotpUsersTable(), "app_id")) { - throw new TenantOrAppNotFoundException(appIdentifier); + throw new TenantOrAppNotFoundException(appIdentifier); } } throw new StorageQueryException(e); @@ -2707,7 +3077,28 @@ public TOTPDevice createDevice_Transaction(TransactionConnection con, AppIdentif } @Override - public TOTPDevice getDeviceByName_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, String deviceName) throws StorageQueryException { + public void createDevices_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + List devices) + throws StorageQueryException, TenantOrAppNotFoundException { + Connection sqlCon = (Connection) con.getConnection(); + try { + TOTPQueries.createDevices_Transaction(this, sqlCon, appIdentifier, devices); + } catch (SQLException e) { + Exception actualException = e; + + if (actualException instanceof PSQLException) { + ServerErrorMessage errMsg = ((PSQLException) actualException).getServerErrorMessage(); + if (isForeignKeyConstraintError(errMsg, Config.getConfig(this).getTotpUsersTable(), "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } + } + throw new StorageQueryException(e); + } + } + + @Override + public TOTPDevice getDeviceByName_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, + String deviceName) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { return TOTPQueries.getDeviceByName_Transaction(this, sqlCon, appIdentifier, userId, deviceName); @@ -2777,7 +3168,7 @@ public void updateDeviceName(AppIdentifier appIdentifier, String userId, String if (e instanceof PSQLException) { ServerErrorMessage errMsg = ((PSQLException) e).getServerErrorMessage(); if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable())) { - throw new DeviceAlreadyExistsException(); + throw new DeviceAlreadyExistsException(); } } throw new StorageQueryException(e); @@ -2856,6 +3247,11 @@ public Set getValidFieldsInConfig() { return PostgreSQLConfig.getValidFields(); } + @Override + public List getPluginConfigFieldsInfo() { + return PostgreSQLConfig.getConfigFieldsInfoForDashboard(this); + } + @Override public void setLogLevels(Set logLevels) { Config.setLogLevels(this, logLevels); @@ -2899,6 +3295,18 @@ public AuthRecipeUserInfo getPrimaryUserById_Transaction(AppIdentifier appIdenti } } + @Override + public List getPrimaryUsersByIds_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + List userIds) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.getPrimaryUserInfosForUserIds_Transaction(this, sqlCon, appIdentifier, userIds); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String email) @@ -2911,6 +3319,19 @@ public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(AppIdentifier ap } } + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByMultipleEmailsOrPhoneNumbersOrThirdparty_Transaction( + AppIdentifier appIdentifier, TransactionConnection con, List emails, List phones, + Map thirdpartyIdToThirdpartyUserId) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByMultipleEmailsOrPhonesOrThirdParty_Transaction(this, sqlCon, + appIdentifier, emails, phones, thirdpartyIdToThirdpartyUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, @@ -2966,6 +3387,17 @@ public void makePrimaryUser_Transaction(AppIdentifier appIdentifier, Transaction } } + @Override + public void makePrimaryUsers_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + List userIds) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + GeneralQueries.makePrimaryUsers_Transaction(this, sqlCon, appIdentifier, userIds); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void linkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String recipeUserId, String primaryUserId) throws StorageQueryException { @@ -2980,7 +3412,21 @@ public void linkAccounts_Transaction(AppIdentifier appIdentifier, TransactionCon } @Override - public void unlinkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String primaryUserId, String recipeUserId) + public void linkMultipleAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + Map recipeUserIdByPrimaryUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + GeneralQueries.linkMultipleAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserIdByPrimaryUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + + } + + @Override + public void unlinkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String primaryUserId, + String recipeUserId) throws StorageQueryException { try { Connection sqlCon = (Connection) con.getConnection(); @@ -3013,7 +3459,8 @@ public boolean checkIfUsesAccountLinking(AppIdentifier appIdentifier) throws Sto } @Override - public int countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { + public int countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(AppIdentifier appIdentifier, long sinceTime) + throws StorageQueryException { try { return ActiveUsersQueries.countUsersActiveSinceAndHasMoreThanOneLoginMethod(this, appIdentifier, sinceTime); } catch (SQLException e) { @@ -3042,11 +3489,13 @@ public UserIdMapping getUserIdMapping_Transaction(TransactionConnection con, App try { Connection sqlCon = (Connection) con.getConnection(); if (isSuperTokensUserId) { - return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId_Transaction(this, sqlCon, appIdentifier, + return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId_Transaction(this, sqlCon, + appIdentifier, userId); } - return UserIdMappingQueries.getUserIdMappingWithExternalUserId_Transaction(this, sqlCon, appIdentifier, userId); + return UserIdMappingQueries.getUserIdMappingWithExternalUserId_Transaction(this, sqlCon, appIdentifier, + userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -3067,7 +3516,29 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A } @Override - public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier appIdentifier) throws StorageQueryException { + public List getMultipleUserIdMapping_Transaction(TransactionConnection connection, + AppIdentifier appIdentifier, List userIds, + boolean isSupertokensIds) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) connection.getConnection(); + List result; + if(isSupertokensIds){ + result = UserIdMappingQueries.getMultipleUserIdMappingWithSupertokensUserId_Transaction(this, + sqlCon, appIdentifier, userIds); + } else { + result = UserIdMappingQueries.getMultipleUserIdMappingWithExternalUserId_Transaction(this, + sqlCon, appIdentifier, userIds); + } + return result; + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier appIdentifier) + throws StorageQueryException { try { return GeneralQueries.getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(this, appIdentifier); } catch (SQLException e) { @@ -3076,7 +3547,9 @@ public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier ap } @Override - public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { + public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(AppIdentifier appIdentifier, + long sinceTime) + throws StorageQueryException { try { return ActiveUsersQueries.countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(this, appIdentifier, sinceTime); @@ -3085,6 +3558,18 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A } } + + @Override + public boolean deleteOAuthClient(AppIdentifier appIdentifier, String clientId) throws StorageQueryException { + try { + return OAuthQueries.deleteOAuthClient(this, clientId, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + + @TestOnly public int getDbActivityCount(String dbname) throws SQLException, StorageQueryException { String QUERY = "SELECT COUNT(*) as c FROM pg_stat_activity WHERE datname = ?;"; @@ -3111,10 +3596,43 @@ public void addBulkImportUsers(AppIdentifier appIdentifier, List if (isPrimaryKeyError(serverErrorMessage, Config.getConfig(this).getBulkImportUsersTable())) { throw new io.supertokens.pluginInterface.bulkimport.exceptions.DuplicateUserIdException(); } - if (isForeignKeyConstraintError(serverErrorMessage, Config.getConfig(this).getBulkImportUsersTable(), "app_id")) { + if (isForeignKeyConstraintError(serverErrorMessage, Config.getConfig(this).getBulkImportUsersTable(), + "app_id")) { throw new TenantOrAppNotFoundException(appIdentifier); } } + } + } + + @Override + public OAuthClient getOAuthClientById(AppIdentifier appIdentifier, String clientId) + throws StorageQueryException, OAuthClientNotFoundException { + try { + OAuthClient client = OAuthQueries.getOAuthClientById(this, clientId, appIdentifier); + if (client == null) { + throw new OAuthClientNotFoundException(); + } + return client; + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void addOrUpdateOauthClient(AppIdentifier appIdentifier, String clientId, String clientSecret, boolean isClientCredentialsOnly, boolean enableRefreshTokenRotation) + throws StorageQueryException, TenantOrAppNotFoundException { + try { + OAuthQueries.addOrUpdateOauthClient(this, appIdentifier, clientId, clientSecret, isClientCredentialsOnly, enableRefreshTokenRotation); + } catch (SQLException e) { + ServerErrorMessage errorMessage = ((PSQLException) e).getServerErrorMessage(); + PostgreSQLConfig config = Config.getConfig(this); + + if (isForeignKeyConstraintError( + errorMessage, + config.getOAuthClientsTable(), + "app_id")) { + throw new TenantOrAppNotFoundException(appIdentifier); + } throw new StorageQueryException(e); } } @@ -3129,6 +3647,15 @@ public List getBulkImportUsers(AppIdentifier appIdentifier, @Non } } + @Override + public List getOAuthClients(AppIdentifier appIdentifier, List clientIds) throws StorageQueryException { + try { + return OAuthQueries.getOAuthClients(this, appIdentifier, clientIds); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void updateBulkImportUserStatus_Transaction(AppIdentifier appIdentifier, TransactionConnection con, @Nonnull String bulkImportUserId, @Nonnull BULK_IMPORT_USER_STATUS status, @Nullable String errorMessage) throws StorageQueryException { @@ -3140,6 +3667,39 @@ public void updateBulkImportUserStatus_Transaction(AppIdentifier appIdentifier, } } + @Override + public void updateMultipleBulkImportUsersStatusToError_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, + @NotNull Map bulkImportUserIdToErrorMessage) + throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + BulkImportQueries.updateMultipleBulkImportUsersStatusToError_Transaction(this, sqlCon, appIdentifier, + bulkImportUserIdToErrorMessage); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean revokeOAuthTokenByGID(AppIdentifier appIdentifier, String gid) throws StorageQueryException { + try { + return OAuthQueries.deleteOAuthSessionByGID(this, appIdentifier, gid); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean revokeOAuthTokenByClientId(AppIdentifier appIdentifier, String clientId) + throws StorageQueryException { + try { + return OAuthQueries.deleteOAuthSessionByClientId(this, appIdentifier, clientId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public List deleteBulkImportUsers(AppIdentifier appIdentifier, @Nonnull String[] bulkImportUserIds) throws StorageQueryException { try { @@ -3149,6 +3709,16 @@ public List deleteBulkImportUsers(AppIdentifier appIdentifier, @Nonnull } } + @Override + public boolean revokeOAuthTokenByJTI(AppIdentifier appIdentifier, String gid, String jti) + throws StorageQueryException { + try { + return OAuthQueries.deleteJTIFromOAuthSession(this, appIdentifier, gid, jti); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public List getBulkImportUsersAndChangeStatusToProcessing(AppIdentifier appIdentifier, @Nonnull Integer limit) throws StorageQueryException { try { @@ -3167,6 +3737,16 @@ public void updateBulkImportUserPrimaryUserId(AppIdentifier appIdentifier, @Nonn } } + @Override + public boolean revokeOAuthTokenBySessionHandle(AppIdentifier appIdentifier, String sessionHandle) + throws StorageQueryException { + try { + return OAuthQueries.deleteOAuthSessionBySessionHandle(this, appIdentifier, sessionHandle); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public long getBulkImportUsersCount(AppIdentifier appIdentifier, @Nullable BULK_IMPORT_USER_STATUS status) throws StorageQueryException { try { @@ -3175,4 +3755,179 @@ public long getBulkImportUsersCount(AppIdentifier appIdentifier, @Nullable BULK_ throw new StorageQueryException(e); } } + + @Override + public void addOAuthM2MTokenForStats(AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws StorageQueryException, OAuthClientNotFoundException { + try { + OAuthQueries.addOAuthM2MTokenForStats(this, appIdentifier, clientId, iat, exp); + } catch (SQLException e) { + ServerErrorMessage errorMessage = ((PSQLException) e).getServerErrorMessage(); + PostgreSQLConfig config = Config.getConfig(this); + + if (isForeignKeyConstraintError( + errorMessage, + config.getOAuthM2MTokensTable(), + "client_id")) { + throw new OAuthClientNotFoundException(); + } + throw new StorageQueryException(e); + } + } + + @Override + public void deleteExpiredOAuthM2MTokens(long exp) throws StorageQueryException { + try { + OAuthQueries.deleteExpiredOAuthM2MTokens(this, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void addOAuthLogoutChallenge(AppIdentifier appIdentifier, String challenge, String clientId, + String postLogoutRedirectionUri, String sessionHandle, String state, long timeCreated) + throws StorageQueryException, DuplicateOAuthLogoutChallengeException, OAuthClientNotFoundException { + try { + OAuthQueries.addOAuthLogoutChallenge(this, appIdentifier, challenge, clientId, postLogoutRedirectionUri, sessionHandle, state, timeCreated); + } catch (SQLException e) { + ServerErrorMessage errorMessage = ((PSQLException) e).getServerErrorMessage(); + PostgreSQLConfig config = Config.getConfig(this); + + if (isPrimaryKeyError(errorMessage, config.getOAuthLogoutChallengesTable())) { + throw new DuplicateOAuthLogoutChallengeException(); + } else if (isForeignKeyConstraintError( + errorMessage, + config.getOAuthLogoutChallengesTable(), + "client_id")) { + throw new OAuthClientNotFoundException(); + } + throw new StorageQueryException(e); + } + } + + @Override + public OAuthLogoutChallenge getOAuthLogoutChallenge(AppIdentifier appIdentifier, String challenge) throws StorageQueryException { + try { + return OAuthQueries.getOAuthLogoutChallenge(this, appIdentifier, challenge); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteOAuthLogoutChallenge(AppIdentifier appIdentifier, String challenge) throws StorageQueryException { + try { + OAuthQueries.deleteOAuthLogoutChallenge(this, appIdentifier, challenge); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteOAuthLogoutChallengesBefore(long time) throws StorageQueryException { + try { + OAuthQueries.deleteOAuthLogoutChallengesBefore(this, time); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void createOrUpdateOAuthSession(AppIdentifier appIdentifier, String gid, String clientId, + String externalRefreshToken, String internalRefreshToken, + String sessionHandle, String jti, long exp) + throws StorageQueryException, OAuthClientNotFoundException { + try { + OAuthQueries.createOrUpdateOAuthSession(this, appIdentifier, gid, clientId, externalRefreshToken, + internalRefreshToken, sessionHandle, jti, exp); + } catch (SQLException e) { + ServerErrorMessage errorMessage = ((PSQLException) e).getServerErrorMessage(); + PostgreSQLConfig config = Config.getConfig(this); + + if (isForeignKeyConstraintError( + errorMessage, + config.getOAuthSessionsTable(), + "client_id")) { + throw new OAuthClientNotFoundException(); + } + throw new StorageQueryException(e); + } + } + + @Override + public String getRefreshTokenMapping(AppIdentifier appIdentifier, String externalRefreshToken) + throws StorageQueryException { + try { + return OAuthQueries.getRefreshTokenMapping(this, appIdentifier, externalRefreshToken); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteExpiredOAuthSessions(long exp) throws StorageQueryException { + try { + OAuthQueries.deleteExpiredOAuthSessions(this, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfOAuthClients(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClients(this, appIdentifier, false); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientCredentialsOnlyOAuthClients(AppIdentifier appIdentifier) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClients(this, appIdentifier, true); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfOAuthM2MTokensCreatedSince(AppIdentifier appIdentifier, long since) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfOAuthM2MTokensCreatedSince(this, appIdentifier, since); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfOAuthM2MTokensAlive(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfOAuthM2MTokensAlive(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean isOAuthTokenRevokedByGID(AppIdentifier appIdentifier, String gid) throws StorageQueryException { + try { + return !OAuthQueries.isOAuthSessionExistsByGID(this, appIdentifier, gid); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean isOAuthTokenRevokedByJTI(AppIdentifier appIdentifier, String gid, String jti) + throws StorageQueryException { + try { + return !OAuthQueries.isOAuthSessionExistsByJTI(this, appIdentifier, gid, jti); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/annotations/DashboardInfo.java b/src/main/java/io/supertokens/storage/postgresql/annotations/DashboardInfo.java new file mode 100644 index 00000000..9bc1ced3 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/annotations/DashboardInfo.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.storage.postgresql.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to provide a description for a configuration fields. To be used on the fields of `CoreConfig` and config + * class in the plugin like `PostgreSQLConfig`, `MysqlConfig`, etc. + */ +@Retention(RetentionPolicy.RUNTIME) +// Make annotation accessible at runtime so that config descriptions can be read from API +@Target(ElementType.FIELD) // Annotation can only be applied to fields +public @interface DashboardInfo { + String description() default ""; + + boolean isOptional() default false; + + String defaultValue() default ""; + + boolean isEditable() default false; +} diff --git a/src/main/java/io/supertokens/storage/postgresql/config/Config.java b/src/main/java/io/supertokens/storage/postgresql/config/Config.java index 41088d73..1ce21e06 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/Config.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/Config.java @@ -53,7 +53,8 @@ private static Config getInstance(Start start) { return (Config) start.getResourceDistributor().getResource(RESOURCE_KEY); } - public static void loadConfig(Start start, JsonObject configJson, Set logLevels, TenantIdentifier tenantIdentifier) + public static void loadConfig(Start start, JsonObject configJson, Set logLevels, + TenantIdentifier tenantIdentifier) throws InvalidConfigException { if (getInstance(start) != null) { return; 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 bd206366..6e915ca8 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -19,21 +19,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import io.supertokens.pluginInterface.ConfigFieldInfo; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; -import io.supertokens.storage.postgresql.annotations.ConnectionPoolProperty; -import io.supertokens.storage.postgresql.annotations.IgnoreForAnnotationCheck; -import io.supertokens.storage.postgresql.annotations.NotConflictingWithinUserPool; -import io.supertokens.storage.postgresql.annotations.UserPoolProperty; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.annotations.*; import java.lang.reflect.Field; import java.net.URI; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; @JsonIgnoreProperties(ignoreUnknown = true) public class PostgreSQLConfig { @@ -44,80 +41,140 @@ public class PostgreSQLConfig { @JsonProperty @ConnectionPoolProperty + @DashboardInfo( + description = "Defines the connection pool size to PostgreSQL. Please see https://github" + + ".com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing", + defaultValue = "10", isOptional = true, isEditable = true) private int postgresql_connection_pool_size = 10; @JsonProperty @UserPoolProperty + @DashboardInfo( + description = "Specify the postgresql host url here. For example: - \"localhost\" - \"192.168.0.1\" - " + + "\"\" - \"example.com\"", + defaultValue = "\"localhost\"", isOptional = true) private String postgresql_host = null; @JsonProperty @UserPoolProperty + @DashboardInfo(description = "Specify the port to use when connecting to PostgreSQL instance.", + defaultValue = "5432", isOptional = true) private int postgresql_port = -1; @JsonProperty @ConnectionPoolProperty + @DashboardInfo( + description = "The PostgreSQL user to use to query the database. If the relevant tables are not already " + + "created by you, this user should have the ability to create new tables. To see the tables " + + "needed, visit: https://supertokens.com/docs/thirdpartyemailpassword/pre-built-ui/setup/database" + + "-setup/postgresql", + defaultValue = "null") private String postgresql_user = null; @JsonProperty @ConnectionPoolProperty + @DashboardInfo( + description = "Password for the PostgreSQL user. If you have not set a password make this an empty string.", + defaultValue = "null") private String postgresql_password = null; @JsonProperty @UserPoolProperty + @DashboardInfo(description = "The database name to store SuperTokens related data.", + defaultValue = "\"supertokens\"", isOptional = true) private String postgresql_database_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo( + description = "A prefix to add to all table names managed by SuperTokens. An \"_\" will be added between " + + "this prefix and the actual table name if the prefix is defined.", + defaultValue = "\"\"", isOptional = true) private String postgresql_table_names_prefix = ""; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo( + description = "Specify the name of the table that will store secret keys and app info necessary for the " + + "functioning sessions.", + defaultValue = "\"key_value\"", isOptional = true) private String postgresql_key_value_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo(description = "Specify the name of the table that will store the session info for users.", + defaultValue = "\"session_info\"", isOptional = true) private String postgresql_session_info_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo( + description = "Specify the name of the table that will store the user information, along with their email" + + " and hashed password.", + defaultValue = "\"emailpassword_users\"", isOptional = true) private String postgresql_emailpassword_users_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo(description = "Specify the name of the table that will store the password reset tokens for users.", + defaultValue = "\"emailpassword_pswd_reset_tokens\"", isOptional = true) private String postgresql_emailpassword_pswd_reset_tokens_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo( + description = "Specify the name of the table that will store the email verification tokens for users.", + defaultValue = "\"emailverification_tokens\"", isOptional = true) private String postgresql_emailverification_tokens_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo(description = "Specify the name of the table that will store the verified email addresses.", + defaultValue = "\"emailverification_verified_emails\"", isOptional = true) private String postgresql_emailverification_verified_emails_table_name = null; @JsonProperty @NotConflictingWithinUserPool + @DashboardInfo(description = "Specify the name of the table that will store the thirdparty recipe users.", + defaultValue = "\"thirdparty_users\"", isOptional = true) private String postgresql_thirdparty_users_table_name = null; @JsonProperty @UserPoolProperty + @DashboardInfo(description = "The schema for tables.", defaultValue = "\"public\"", isOptional = true) private String postgresql_table_schema = "public"; @JsonProperty @IgnoreForAnnotationCheck + @DashboardInfo( + description = "Specify the PostgreSQL connection URI in the following format: " + + "postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2... Values provided " + + "via other configs will override values provided by this config.", + defaultValue = "null", isOptional = true) private String postgresql_connection_uri = null; @ConnectionPoolProperty + @DashboardInfo(description = "The connection attributes of the PostgreSQL database.", + defaultValue = "\"allowPublicKeyRetrieval=true\"", isOptional = true) private String postgresql_connection_attributes = "allowPublicKeyRetrieval=true"; @ConnectionPoolProperty + @DashboardInfo(description = "The scheme of the PostgreSQL database.", defaultValue = "\"postgresql\"", + isOptional = true) private String postgresql_connection_scheme = "postgresql"; @JsonProperty @ConnectionPoolProperty + @DashboardInfo(description = "Timeout in milliseconds for the idle connections to be closed.", + defaultValue = "60000", isOptional = true, isEditable = true) private long postgresql_idle_connection_timeout = 60000; @JsonProperty @ConnectionPoolProperty + @DashboardInfo( + description = "Minimum number of idle connections to be kept active. If not set, minimum idle connections" + + " will be same as the connection pool size.", + defaultValue = "null", isOptional = true, isEditable = true) private Integer postgresql_minimum_idle_connections = null; @IgnoreForAnnotationCheck @@ -134,6 +191,69 @@ public static Set getValidFields() { return validFields; } + public static ArrayList getConfigFieldsInfoForDashboard(Start start) { + ArrayList result = new ArrayList(); + + JsonObject tenantConfig = new Gson().toJsonTree(Config.getConfig(start)).getAsJsonObject(); + + PostgreSQLConfig defaultConfigObj = new PostgreSQLConfig(); + try { + defaultConfigObj.validateAndNormalise(true); // skip validation and just populate defaults + } catch (InvalidConfigException e) { + throw new IllegalStateException(e); // should never happen + } + + JsonObject defaultConfig = new Gson().toJsonTree(defaultConfigObj).getAsJsonObject(); + + for (String fieldId : PostgreSQLConfig.getValidFields()) { + try { + Field field = PostgreSQLConfig.class.getDeclaredField(fieldId); + if (!field.isAnnotationPresent(DashboardInfo.class)) { + continue; + } + + if (field.getName().endsWith("_table_name")) { + continue; // do not show + } + + String key = field.getName(); + String description = field.isAnnotationPresent(DashboardInfo.class) + ? field.getAnnotation(DashboardInfo.class).description() + : ""; + boolean isDifferentAcrossTenants = true; + + String valueType = null; + + Class fieldType = field.getType(); + + if (fieldType == String.class) { + valueType = "string"; + } else if (fieldType == boolean.class) { + valueType = "boolean"; + } else if (fieldType == int.class || fieldType == long.class || fieldType == Integer.class) { + valueType = "number"; + } else { + throw new RuntimeException("Unknown field type " + fieldType.getName()); + } + + JsonElement value = tenantConfig.get(field.getName()); + + JsonElement defaultValue = defaultConfig.get(field.getName()); + boolean isNullable = defaultValue == null; + + boolean isEditable = field.getAnnotation(DashboardInfo.class).isEditable(); + + result.add(new ConfigFieldInfo( + key, valueType, value, description, isDifferentAcrossTenants, + null, isNullable, defaultValue, true, isEditable)); + + } catch (NoSuchFieldException e) { + continue; + } + } + return result; + } + public String getTableSchema() { return postgresql_table_schema; } @@ -314,6 +434,7 @@ public String getDashboardSessionsTable() { return addSchemaAndPrefixToTableName("dashboard_user_sessions"); } + public String getTotpUsersTable() { return addSchemaAndPrefixToTableName("totp_users"); } @@ -325,6 +446,21 @@ public String getTotpUserDevicesTable() { public String getTotpUsedCodesTable() { return addSchemaAndPrefixToTableName("totp_used_codes"); } + public String getOAuthClientsTable() { + return addSchemaAndPrefixToTableName("oauth_clients"); + } + + public String getOAuthM2MTokensTable() { + return addSchemaAndPrefixToTableName("oauth_m2m_tokens"); + } + + public String getOAuthSessionsTable() { + return addSchemaAndPrefixToTableName("oauth_sessions"); + } + + public String getOAuthLogoutChallengesTable() { + return addSchemaAndPrefixToTableName("oauth_logout_challenges"); + } public String getBulkImportUsersTable() { return addSchemaAndPrefixToTableName("bulk_import_users"); @@ -343,41 +479,48 @@ private String addSchemaToTableName(String tableName) { } public void validateAndNormalise() throws InvalidConfigException { + validateAndNormalise(false); + } + + private void validateAndNormalise(boolean skipValidation) throws InvalidConfigException { if (isValidAndNormalised) { return; } - if (postgresql_connection_uri != null) { - try { - URI ignored = URI.create(postgresql_connection_uri); - } catch (Exception e) { - throw new InvalidConfigException( - "The provided postgresql connection URI has an incorrect format. Please use a format like " - + "postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2..."); - } - } else { - if (this.getUser() == null) { - throw new InvalidConfigException( - "'postgresql_user' and 'postgresql_connection_uri' are not set. Please set at least one of " - + "these values"); + if (!skipValidation) { + if (postgresql_connection_uri != null) { + try { + URI ignored = URI.create(postgresql_connection_uri); + } catch (Exception e) { + throw new InvalidConfigException( + "The provided postgresql connection URI has an incorrect format. Please use a format like " + + + "postgresql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2..."); + } + } else { + if (this.getUser() == null) { + throw new InvalidConfigException( + "'postgresql_user' and 'postgresql_connection_uri' are not set. Please set at least one of " + + "these values"); + } } - } - if (postgresql_connection_pool_size <= 0) { - throw new InvalidConfigException( - "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); - } - - if (postgresql_minimum_idle_connections != null) { - if (postgresql_minimum_idle_connections < 0) { + if (postgresql_connection_pool_size <= 0) { throw new InvalidConfigException( - "'postgresql_minimum_idle_connections' must be >= 0"); + "'postgresql_connection_pool_size' in the config.yaml file must be > 0"); } - if (postgresql_minimum_idle_connections > postgresql_connection_pool_size) { - throw new InvalidConfigException( - "'postgresql_minimum_idle_connections' must be less than or equal to " - + "'postgresql_connection_pool_size'"); + if (postgresql_minimum_idle_connections != null) { + if (postgresql_minimum_idle_connections < 0) { + throw new InvalidConfigException( + "'postgresql_minimum_idle_connections' must be >= 0"); + } + + if (postgresql_minimum_idle_connections > postgresql_connection_pool_size) { + throw new InvalidConfigException( + "'postgresql_minimum_idle_connections' must be less than or equal to " + + "'postgresql_connection_pool_size'"); + } } } @@ -526,21 +669,26 @@ public void validateAndNormalise() throws InvalidConfigException { } if (postgresql_emailpassword_pswd_reset_tokens_table_name != null) { - postgresql_emailpassword_pswd_reset_tokens_table_name = addSchemaToTableName(postgresql_emailpassword_pswd_reset_tokens_table_name); + postgresql_emailpassword_pswd_reset_tokens_table_name = addSchemaToTableName( + postgresql_emailpassword_pswd_reset_tokens_table_name); } else { - postgresql_emailpassword_pswd_reset_tokens_table_name = addSchemaAndPrefixToTableName("emailpassword_pswd_reset_tokens"); + postgresql_emailpassword_pswd_reset_tokens_table_name = addSchemaAndPrefixToTableName( + "emailpassword_pswd_reset_tokens"); } if (postgresql_emailverification_tokens_table_name != null) { - postgresql_emailverification_tokens_table_name = addSchemaToTableName(postgresql_emailverification_tokens_table_name); + postgresql_emailverification_tokens_table_name = addSchemaToTableName( + postgresql_emailverification_tokens_table_name); } else { postgresql_emailverification_tokens_table_name = addSchemaAndPrefixToTableName("emailverification_tokens"); } if (postgresql_emailverification_verified_emails_table_name != null) { - postgresql_emailverification_verified_emails_table_name = addSchemaToTableName(postgresql_emailverification_verified_emails_table_name); + postgresql_emailverification_verified_emails_table_name = addSchemaToTableName( + postgresql_emailverification_verified_emails_table_name); } else { - postgresql_emailverification_verified_emails_table_name = addSchemaAndPrefixToTableName("emailverification_verified_emails"); + postgresql_emailverification_verified_emails_table_name = addSchemaAndPrefixToTableName( + "emailverification_verified_emails"); } if (postgresql_thirdparty_users_table_name != null) { @@ -592,10 +740,11 @@ public String getConnectionPoolId() { try { String fieldName = field.getName(); String fieldValue = field.get(this) != null ? field.get(this).toString() : null; - if(fieldValue == null) { + if (fieldValue == null) { continue; } - // To ensure a unique connectionPoolId we include the database password and use the "|db_pass|" identifier. + // To ensure a unique connectionPoolId we include the database password and use the "|db_pass|" + // identifier. // This facilitates easy removal of the password from logs when necessary. if (fieldName.equals("postgresql_password")) { connectionPoolId.append("|db_pass|" + fieldValue + "|db_pass"); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java index 3a39c384..50edb15c 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ActiveUsersQueries.java @@ -5,9 +5,13 @@ import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; +import org.jetbrains.annotations.TestOnly; import java.sql.Connection; import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -34,6 +38,11 @@ static String getQueryToCreateAppIdIndexForUserLastActiveTable(Start start) { + Config.getConfig(start).getUserLastActiveTable() + "(app_id);"; } + public static String getQueryToCreateLastActiveTimeIndexForUserLastActiveTable(Start start) { + return "CREATE INDEX IF NOT EXISTS user_last_active_last_active_time_index ON " + + Config.getConfig(start).getUserLastActiveTable() + "(last_active_time DESC, app_id DESC);"; + } + public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getUserLastActiveTable() @@ -50,7 +59,8 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier }); } - public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, long sinceTime) + public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, + long sinceTime) throws SQLException, StorageQueryException { // TODO: Active users are present only on public tenant and MFA users may be present on different storages String QUERY = "SELECT count(1) as c FROM (" @@ -89,6 +99,22 @@ public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, }); } + @TestOnly + public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId, long timestamp) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() + + + "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, user_id) DO UPDATE SET " + + "last_active_time = ?"; + + return update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setLong(3, timestamp); + pst.setLong(4, timestamp); + }); + } + public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException { String QUERY = "SELECT last_active_time FROM " + Config.getConfig(start).getUserLastActiveTable() @@ -109,6 +135,30 @@ public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifie } } + public static Map getLastActiveByMultipleUserIds(Start start, AppIdentifier appIdentifier, List userIds) + throws StorageQueryException { + String QUERY = "SELECT user_id, last_active_time FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND user_id IN ( " + Utils.generateCommaSeperatedQuestionMarks(userIds.size())+ " )"; + + try { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < userIds.size(); i++) { + pst.setString(2+i, userIds.get(i)); + } + }, res -> { + Map lastActiveByUserIds = new HashMap<>(); + if (res.next()) { + String userId = res.getString("user_id"); + lastActiveByUserIds.put(userId, res.getLong("last_active_time")); + } + return lastActiveByUserIds; + }); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + public static void deleteUserActive_Transaction(Connection con, Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { @@ -121,30 +171,32 @@ public static void deleteUserActive_Transaction(Connection con, Start start, App }); } - public static int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) + public static int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(Start start, + AppIdentifier appIdentifier, + long sinceTime) throws SQLException, StorageQueryException { // TODO: Active users are present only on public tenant and MFA users may be present on different storages String QUERY = - "SELECT COUNT (DISTINCT user_id) as c FROM (" - + " (" // users with more than one login method - + " SELECT primary_or_recipe_user_id AS user_id FROM (" - + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" - + " FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND primary_or_recipe_user_id IN (" - + " SELECT user_id FROM " + getConfig(start).getUserLastActiveTable() - + " WHERE app_id = ? AND last_active_time >= ?" - + " )" - + " GROUP BY (app_id, primary_or_recipe_user_id)" - + " ) AS nloginmethods" - + " WHERE num_login_methods > 1" - + " ) UNION (" // TOTP users - + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() - + " WHERE app_id = ? AND user_id IN (" - + " SELECT user_id FROM " + getConfig(start).getUserLastActiveTable() - + " WHERE app_id = ? AND last_active_time >= ?" - + " )" - + " )" - + ") AS all_users"; + "SELECT COUNT (DISTINCT user_id) as c FROM (" + + " (" // users with more than one login method + + " SELECT primary_or_recipe_user_id AS user_id FROM (" + + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND primary_or_recipe_user_id IN (" + + " SELECT user_id FROM " + getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " GROUP BY (app_id, primary_or_recipe_user_id)" + + " ) AS nloginmethods" + + " WHERE num_login_methods > 1" + + " ) UNION (" // TOTP users + + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() + + " WHERE app_id = ? AND user_id IN (" + + " SELECT user_id FROM " + getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " )" + + ") AS all_users"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/BulkImportQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/BulkImportQueries.java index 0a01fd4f..f4874540 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/BulkImportQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/BulkImportQueries.java @@ -16,18 +16,6 @@ package io.supertokens.storage.postgresql.queries; -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.bulkimport.BulkImportStorage.BULK_IMPORT_USER_STATUS; import io.supertokens.pluginInterface.bulkimport.BulkImportUser; @@ -38,6 +26,19 @@ import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; + public class BulkImportQueries { static String getQueryToCreateBulkImportUsersTable(Start start) { String schema = Config.getConfig(start).getTableSchema(); @@ -122,6 +123,32 @@ public static void updateBulkImportUserStatus_Transaction(Start start, Connectio }); } + public static void updateMultipleBulkImportUsersStatusToError_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + @Nonnull Map bulkImportUserIdToErrorMessage) + throws SQLException { + BULK_IMPORT_USER_STATUS errorStatus = BULK_IMPORT_USER_STATUS.FAILED; + String query = "UPDATE " + Config.getConfig(start).getBulkImportUsersTable() + + " SET status = ?, error_msg = ?, updated_at = ? WHERE app_id = ? and id = ?"; + + PreparedStatement setErrorStatement = con.prepareStatement(query); + + int counter = 0; + for(String bulkImportUserId : bulkImportUserIdToErrorMessage.keySet()){ + setErrorStatement.setString(1, errorStatus.toString()); + setErrorStatement.setString(2, bulkImportUserIdToErrorMessage.get(bulkImportUserId)); + setErrorStatement.setLong(3, System.currentTimeMillis()); + setErrorStatement.setString(4, appIdentifier.getAppId()); + setErrorStatement.setString(5, bulkImportUserId); + setErrorStatement.addBatch(); + + if(counter % 100 == 0) { + setErrorStatement.executeBatch(); + } + } + + setErrorStatement.executeBatch(); + } + public static List getBulkImportUsersAndChangeStatusToProcessing(Start start, AppIdentifier appIdentifier, @Nonnull Integer limit) @@ -130,14 +157,14 @@ public static List getBulkImportUsersAndChangeStatusToProcessing return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { - // NOTE: On average, we take about 66 seconds to process 1000 users. If, for any reason, the bulk import users were marked as processing but couldn't be processed within 10 minutes, we'll attempt to process them again. // "FOR UPDATE" ensures that multiple cron jobs don't read the same rows simultaneously. // If one process locks the first 1000 rows, others will wait for the lock to be released. // "SKIP LOCKED" allows other processes to skip locked rows and select the next 1000 available rows. String selectQuery = "SELECT * FROM " + Config.getConfig(start).getBulkImportUsersTable() + " WHERE app_id = ?" - + " AND (status = 'NEW' OR (status = 'PROCESSING' AND updated_at < (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000) - 10 * 60 * 1000))" /* 10 mins */ + //+ " AND (status = 'NEW' OR (status = 'PROCESSING' AND updated_at < (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000) - 10 * 60 * 1000))" /* 10 mins */ + + " AND (status = 'NEW' OR status = 'PROCESSING' )" + " LIMIT ? FOR UPDATE SKIP LOCKED"; List bulkImportUsers = new ArrayList<>(); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java index 135d2f7a..89e85607 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/DashboardQueries.java @@ -51,7 +51,7 @@ public static String getQueryToCreateDashboardUsersTable(Start start) { + " PRIMARY KEY (app_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, dashboardUsersTable, "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -119,7 +119,8 @@ public static boolean deleteDashboardUserWithUserId(Start start, AppIdentifier a } - public static DashboardUser[] getAllDashBoardUsers(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + public static DashboardUser[] getAllDashBoardUsers(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getDashboardUsersTable() + " WHERE app_id = ? ORDER BY time_joined ASC"; return QueryExecutorTemplate.execute(start, QUERY, @@ -172,8 +173,9 @@ public static boolean updateDashboardUsersPasswordWithUserId_Transaction(Start s return rowsUpdated > 0; } - public static void createDashboardSession(Start start, AppIdentifier appIdentifier, String userId, String sessionId, long timeCreated, - long expiry) throws SQLException, StorageQueryException { + public static void createDashboardSession(Start start, AppIdentifier appIdentifier, String userId, String sessionId, + long timeCreated, + long expiry) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getDashboardSessionsTable() + "(app_id, user_id, session_id, time_created, expiry)" + " VALUES(?, ?, ?, ?, ?)"; QueryExecutorTemplate.update(start, QUERY, pst -> { @@ -185,7 +187,8 @@ public static void createDashboardSession(Start start, AppIdentifier appIdentifi }); } - public static DashboardSessionInfo getSessionInfoWithSessionId(Start start, AppIdentifier appIdentifier, String sessionId) + public static DashboardSessionInfo getSessionInfoWithSessionId(Start start, AppIdentifier appIdentifier, + String sessionId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getDashboardSessionsTable() + " WHERE app_id = ? AND session_id = ?"; @@ -200,7 +203,8 @@ public static DashboardSessionInfo getSessionInfoWithSessionId(Start start, AppI }); } - public static DashboardSessionInfo[] getAllSessionsForUserId(Start start, AppIdentifier appIdentifier, String userId) + public static DashboardSessionInfo[] getAllSessionsForUserId(Start start, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getDashboardSessionsTable() + " WHERE app_id = ? AND user_id = ?"; @@ -234,7 +238,8 @@ public static DashboardUser getDashboardUserByEmail(Start start, AppIdentifier a }); } - public static boolean deleteDashboardUserSessionWithSessionId(Start start, AppIdentifier appIdentifier, String sessionId) + public static boolean deleteDashboardUserSessionWithSessionId(Start start, AppIdentifier appIdentifier, + String sessionId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getDashboardSessionsTable() + " WHERE app_id = ? AND session_id = ?"; 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 55bb51c4..deb2bfe8 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java @@ -19,19 +19,19 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.EmailPasswordImportUser; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -285,7 +285,9 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden { // 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, primary_or_recipe_user_time_joined)" + + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + + "primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -334,6 +336,83 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden }); } + public static void signUpMultipleForBulkImport_Transaction(Start start, Connection sqlCon, List usersToSignUp) + throws StorageQueryException, StorageTransactionLogicException, SQLException { + try { + String app_id_to_user_id_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; + + String all_auth_recipe_users_QUERY = "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(?, ?, ?, ?, ?, ?, ?)"; + + String emailpassword_users_QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUsersTable() + + "(app_id, user_id, email, password_hash, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + String emailpassword_users_to_tenant_QUERY = + "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + + "(app_id, tenant_id, user_id, email)" + " VALUES(?, ?, ?, ?)"; + + PreparedStatement appIdToUserId = sqlCon.prepareStatement(app_id_to_user_id_QUERY); + PreparedStatement allAuthRecipeUsers = sqlCon.prepareStatement(all_auth_recipe_users_QUERY); + PreparedStatement emailPasswordUsers = sqlCon.prepareStatement(emailpassword_users_QUERY); + PreparedStatement emailPasswordUsersToTenant = sqlCon.prepareStatement(emailpassword_users_to_tenant_QUERY); + + int counter = 0; + for (EmailPasswordImportUser user : usersToSignUp) { + String userId = user.userId; + TenantIdentifier tenantIdentifier = user.tenantIdentifier; + + appIdToUserId.setString(1, tenantIdentifier.getAppId()); + appIdToUserId.setString(2, userId); + appIdToUserId.setString(3, userId); + appIdToUserId.setString(4, EMAIL_PASSWORD.toString()); + appIdToUserId.addBatch(); + + + allAuthRecipeUsers.setString(1, tenantIdentifier.getAppId()); + allAuthRecipeUsers.setString(2, tenantIdentifier.getTenantId()); + allAuthRecipeUsers.setString(3, userId); + allAuthRecipeUsers.setString(4, userId); + allAuthRecipeUsers.setString(5, EMAIL_PASSWORD.toString()); + allAuthRecipeUsers.setLong(6, user.timeJoinedMSSinceEpoch); + allAuthRecipeUsers.setLong(7, user.timeJoinedMSSinceEpoch); + allAuthRecipeUsers.addBatch(); + + emailPasswordUsers.setString(1, tenantIdentifier.getAppId()); + emailPasswordUsers.setString(2, userId); + emailPasswordUsers.setString(3, user.email); + emailPasswordUsers.setString(4, user.passwordHash); + emailPasswordUsers.setLong(5, user.timeJoinedMSSinceEpoch); + emailPasswordUsers.addBatch(); + + emailPasswordUsersToTenant.setString(1, tenantIdentifier.getAppId()); + emailPasswordUsersToTenant.setString(2, tenantIdentifier.getTenantId()); + emailPasswordUsersToTenant.setString(3, userId); + emailPasswordUsersToTenant.setString(4, user.email); + emailPasswordUsersToTenant.addBatch(); + counter++; + if (counter % 100 == 0) { + appIdToUserId.executeBatch(); + allAuthRecipeUsers.executeBatch(); + emailPasswordUsers.executeBatch(); + emailPasswordUsersToTenant.executeBatch(); + } + } + + //execute the remaining ones + appIdToUserId.executeBatch(); + allAuthRecipeUsers.executeBatch(); + emailPasswordUsers.executeBatch(); + emailPasswordUsersToTenant.executeBatch(); + + sqlCon.commit(); + } catch (SQLException throwables) { + throw new StorageTransactionLogicException(throwables); + } + } + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, String userId, boolean deleteUserIdMappingToo) throws StorageQueryException, SQLException { @@ -375,8 +454,10 @@ public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIde } } - private static UserInfoPartial getUserInfoUsingId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, - String id) throws SQLException, StorageQueryException { + private static UserInfoPartial getUserInfoUsingId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String id) + throws SQLException, StorageQueryException { // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on // app_id_to_user_id table String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " @@ -426,7 +507,7 @@ public static List getUsersInfoUsingIdList(Start start, Set } public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, - AppIdentifier appIdentifier) + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { if (ids.size() > 0) { // No need to filter based on tenantId because the id list is already filtered for a tenant @@ -456,6 +537,7 @@ public static List getUsersInfoUsingIdList_Transaction(Start start, } return Collections.emptyList(); } + public static String lockEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String email) @@ -474,8 +556,32 @@ public static String lockEmail_Transaction(Start start, Connection con, }); } + public static List lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List emails) + throws StorageQueryException, SQLException { + if(emails == null || emails.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT user_id FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE app_id = ? AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ") FOR UPDATE"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < emails.size(); i++) { + pst.setString(2 + i, emails.get(i)); + } + }, result -> { + List results = new ArrayList<>(); + while (result.next()) { + results.add(result.getString("user_id")); + } + return results; + }); + } + public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, - String email) + 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" + @@ -495,8 +601,9 @@ public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier te }); } - public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, - String email) + public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String email) throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep" + @@ -516,6 +623,33 @@ public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, }); } + public static List getPrimaryUserIdsUsingMultipleEmails_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List emails) + throws StorageQueryException, SQLException { + if(emails.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.email IN ( " + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + " )"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < emails.size(); i++) { + pst.setString(2+i, emails.get(i)); + } + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException, UnknownUserIdException { @@ -526,11 +660,14 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC throw new UnknownUserIdException(); } - GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, + sqlCon, tenantIdentifier.toAppIdentifier(), userId); { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, " + + "recipe_id, time_joined, primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; GeneralQueries.AccountLinkingInfo finalAccountLinkingInfo = accountLinkingInfo; @@ -545,14 +682,16 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC pst.setLong(8, userInfo.timeJoined); }); - GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), finalAccountLinkingInfo.primaryUserId); + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), + finalAccountLinkingInfo.primaryUserId); } { // emailpassword_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + "(app_id, tenant_id, user_id, email)" + " VALUES(?, ?, ?, ?) " + " ON CONFLICT ON CONSTRAINT " - + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getEmailPasswordUserToTenantTable(), null, "pkey") + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), + getConfig(start).getEmailPasswordUserToTenantTable(), null, "pkey") + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java index 6fd00660..fc3fa372 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/EmailVerificationQueries.java @@ -28,6 +28,7 @@ import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -124,6 +125,48 @@ public static void updateUsersIsEmailVerified_Transaction(Start start, Connectio } } + public static void updateMultipleUsersIsEmailVerified_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + Map emailToUserIds, + boolean isEmailVerified) + throws SQLException, StorageQueryException { + + if (isEmailVerified) { + String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTable() + + "(app_id, user_id, email) VALUES(?, ?, ?)"; + PreparedStatement insertQuery = con.prepareStatement(QUERY); + int counter = 0; + for(Map.Entry emailToUser : emailToUserIds.entrySet()){ + insertQuery.setString(1, appIdentifier.getAppId()); + insertQuery.setString(2, emailToUser.getKey()); + insertQuery.setString(3, emailToUser.getValue()); + insertQuery.addBatch(); + + counter++; + if (counter % 100 == 0) { + insertQuery.executeBatch(); + } + } + insertQuery.executeBatch(); + } else { + String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id = ? AND email = ?"; + PreparedStatement deleteQuery = con.prepareStatement(QUERY); + int counter = 0; + for (Map.Entry emailToUser : emailToUserIds.entrySet()) { + deleteQuery.setString(1, appIdentifier.getAppId()); + deleteQuery.setString(2, emailToUser.getValue()); + deleteQuery.setString(3, emailToUser.getKey()); + deleteQuery.addBatch(); + + counter++; + if (counter % 100 == 0) { + deleteQuery.executeBatch(); + } + } + deleteQuery.executeBatch(); + } + } + public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String userId, @@ -270,8 +313,10 @@ public static List isEmailVerified_transaction(Start start, Connection s // We have external user id stored in the email verification table, so we need to fetch the mapped userids for // calculating the verified emails - HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds_Transaction(start, - sqlCon, appIdentifier, supertokensUserIds); + HashMap supertokensUserIdToExternalUserIdMap = + UserIdMappingQueries.getUserIdMappingWithUserIds_Transaction( + start, + sqlCon, appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); @@ -298,7 +343,8 @@ public static List isEmailVerified_transaction(Start start, Connection s } String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) + + + " WHERE app_id = ? AND user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; return execute(sqlCon, QUERY, pst -> { @@ -324,7 +370,7 @@ public static List isEmailVerified_transaction(Start start, Connection s } public static List isEmailVerified(Start start, AppIdentifier appIdentifier, - List userIdAndEmail) + List userIdAndEmail) throws SQLException, StorageQueryException { if (userIdAndEmail.isEmpty()) { @@ -339,7 +385,8 @@ public static List isEmailVerified(Start start, AppIdentifier appIdentif } // We have external user id stored in the email verification table, so we need to fetch the mapped userids for // calculating the verified emails - HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds(start, + HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds( + start, appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); @@ -365,7 +412,8 @@ public static List isEmailVerified(Start start, AppIdentifier appIdentif supertokensOrExternalUserIdToEmailMap.put(supertokensOrExternalUserId, ue.email); } String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) + + + " WHERE app_id = ? AND user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -476,7 +524,47 @@ public static boolean isUserIdBeingUsedForEmailVerification(Start start, AppIden } } - public static void updateIsEmailVerifiedToExternalUserId(Start start, AppIdentifier appIdentifier, String supertokensUserId, String externalUserId) + public static Set findUserIdsBeingUsedForEmailVerification(Start start, AppIdentifier appIdentifier, List userIds) + throws SQLException, StorageQueryException { + + Set foundUserIds = new HashSet<>(); + + String email_verificiation_tokens_QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +")"; + + foundUserIds.addAll(execute(start, email_verificiation_tokens_QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < userIds.size(); i++) { + pst.setString(2 + i, userIds.get(i)); + } + }, result -> { + Set userIdsFound = new HashSet<>(); + while (result.next()) { + userIdsFound.add(result.getString("user_id")); + } + return userIdsFound; + })); + + String email_verification_table_QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +")"; + + foundUserIds.addAll(execute(start, email_verification_table_QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < userIds.size(); i++) { + pst.setString(2 + i, userIds.get(i)); + } + }, result -> { + Set userIdsFound = new HashSet<>(); + while (result.next()) { + userIdsFound.add(result.getString("user_id")); + } + return userIdsFound; + })); + return foundUserIds; + } + + public static void updateIsEmailVerifiedToExternalUserId(Start start, AppIdentifier appIdentifier, + String supertokensUserId, String externalUserId) throws StorageQueryException { try { start.startTransaction((TransactionConnection con) -> { @@ -511,6 +599,52 @@ public static void updateIsEmailVerifiedToExternalUserId(Start start, AppIdentif } } + public static void updateMultipleIsEmailVerifiedToExternalUserIds(Start start, AppIdentifier appIdentifier, + Map supertokensUserIdToExternalUserId) + throws StorageQueryException { + try { + start.startTransaction((TransactionConnection con) -> { + Connection sqlCon = (Connection) con.getConnection(); + try { + String update_email_verification_table_query = "UPDATE " + getConfig(start).getEmailVerificationTable() + + " SET user_id = ? WHERE app_id = ? AND user_id = ?"; + String update_email_verification_tokens_table_query = "UPDATE " + getConfig(start).getEmailVerificationTokensTable() + + " SET user_id = ? WHERE app_id = ? AND user_id = ?"; + PreparedStatement updateEmailVerificationQuery = sqlCon.prepareStatement(update_email_verification_table_query); + PreparedStatement updateEmailVerificationTokensQuery = sqlCon.prepareStatement(update_email_verification_tokens_table_query); + + int counter = 0; + for (String supertokensUserId : supertokensUserIdToExternalUserId.keySet()){ + updateEmailVerificationQuery.setString(1, supertokensUserIdToExternalUserId.get(supertokensUserId)); + updateEmailVerificationQuery.setString(2, appIdentifier.getAppId()); + updateEmailVerificationQuery.setString(3, supertokensUserId); + updateEmailVerificationQuery.addBatch(); + + updateEmailVerificationTokensQuery.setString(1, supertokensUserIdToExternalUserId.get(supertokensUserId)); + updateEmailVerificationTokensQuery.setString(2, appIdentifier.getAppId()); + updateEmailVerificationTokensQuery.setString(3, supertokensUserId); + updateEmailVerificationTokensQuery.addBatch(); + + counter++; + if(counter % 100 == 0) { + updateEmailVerificationQuery.executeBatch(); + updateEmailVerificationTokensQuery.executeBatch(); + } + } + updateEmailVerificationQuery.executeBatch(); + updateEmailVerificationTokensQuery.executeBatch(); + + } catch (SQLException e) { + throw new StorageTransactionLogicException(e); + } + + return null; + }); + } catch (StorageTransactionLogicException e) { + throw new StorageQueryException(e.actualException); + } + } + private static class EmailVerificationTokenInfoRowMapper implements RowMapper { private static final EmailVerificationTokenInfoRowMapper INSTANCE = new EmailVerificationTokenInfoRowMapper(); 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 d0d9f7de..b0c5c7ee 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -34,6 +34,7 @@ import org.jetbrains.annotations.TestOnly; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -56,13 +57,15 @@ public class GeneralQueries { - private static boolean doesTableExists(Start start, Connection connection, String tableName) throws SQLException, StorageQueryException { + private static boolean doesTableExists(Start start, Connection connection, String tableName) + throws SQLException, StorageQueryException { try { String QUERY = "SELECT 1 FROM " + tableName + " LIMIT 1"; execute(connection, QUERY, NO_OP_SETTER, result -> null); return true; } catch (SQLException | StorageQueryException e) { - if (e.getMessage().contains("relation") && e.getMessage().contains(tableName) && e.getMessage().contains("does not exist")) { + if (e.getMessage().contains("relation") && e.getMessage().contains(tableName) && + e.getMessage().contains("does not exist")) { return false; } throw e; @@ -89,7 +92,8 @@ static String getQueryToCreateUsersTable(Start start) { + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "primary_or_recipe_user_id", "fkey") + " FOREIGN KEY(app_id, primary_or_recipe_user_id)" - + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, usersTable, "user_id", "fkey") + " FOREIGN KEY(app_id, user_id)" + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + @@ -100,8 +104,16 @@ static String getQueryToCreateUsersTable(Start start) { public static String getQueryToCreateUserIdIndexForUsersTable(Start start) { return "CREATE INDEX IF NOT EXISTS all_auth_recipe_user_id_index ON " + + Config.getConfig(start).getUsersTable() + "(user_id);"; + } + public static String getQueryToCreateUserIdAppIdIndexForUsersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS all_auth_recipe_user_id_app_id_index ON " + Config.getConfig(start).getUsersTable() + "(app_id, user_id);"; } + public static String getQueryToCreateAppIdIndexForUsersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS all_auth_recipe_user_app_id_index ON " + + Config.getConfig(start).getUsersTable() + "(app_id);"; + } public static String getQueryToCreateTenantIdIndexForUsersTable(Start start) { return "CREATE INDEX IF NOT EXISTS all_auth_recipe_tenant_id_index ON " @@ -120,12 +132,16 @@ static String getQueryToCreateUserPaginationIndex2(Start start) { static String getQueryToCreateUserPaginationIndex3(Start start) { return "CREATE INDEX all_auth_recipe_users_pagination_index3 ON " + Config.getConfig(start).getUsersTable() - + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);"; + + + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id " + + "DESC);"; } static String getQueryToCreateUserPaginationIndex4(Start start) { return "CREATE INDEX all_auth_recipe_users_pagination_index4 ON " + Config.getConfig(start).getUsersTable() - + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);"; + + + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id " + + "DESC);"; } static String getQueryToCreatePrimaryUserId(Start start) { @@ -221,7 +237,8 @@ private static String getQueryToCreateAppIdToUserIdTable(Start start) { + " PRIMARY KEY (app_id, user_id), " + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "primary_or_recipe_user_id", "fkey") + " FOREIGN KEY(app_id, primary_or_recipe_user_id)" - + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE," + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + + " (app_id, user_id) ON DELETE CASCADE," + "CONSTRAINT " + Utils.getConstraintName(schema, appToUserTable, "app_id", "fkey") + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" @@ -239,6 +256,11 @@ static String getQueryToCreatePrimaryUserIdIndexForAppIdToUserIdTable(Start star + Config.getConfig(start).getAppIdToUserIdTable() + "(primary_or_recipe_user_id, app_id);"; } + static String getQueryToCreateUserIdIndexForAppIdToUserIdTable(Start start) { + return "CREATE INDEX IF NOT EXISTS app_id_to_user_id_user_id_index ON " + + Config.getConfig(start).getAppIdToUserIdTable() + "(user_id, app_id);"; + } + public static void createTablesIfNotExists(Start start, Connection con) throws SQLException, StorageQueryException { int numberOfRetries = 0; boolean retry = true; @@ -273,6 +295,7 @@ public static void createTablesIfNotExists(Start start, Connection con) throws S // index update(con, getQueryToCreateAppIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); update(con, getQueryToCreatePrimaryUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); + update(con, getQueryToCreateUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); } if (!doesTableExists(start, con, Config.getConfig(start).getUsersTable())) { @@ -295,6 +318,8 @@ public static void createTablesIfNotExists(Start start, Connection con) throws S // Index update(con, ActiveUsersQueries.getQueryToCreateAppIdIndexForUserLastActiveTable(start), NO_OP_SETTER); + update(con, ActiveUsersQueries.getQueryToCreateLastActiveTimeIndexForUserLastActiveTable(start), + NO_OP_SETTER); } if (!doesTableExists(start, con, Config.getConfig(start).getAccessTokenSigningKeysTable())) { @@ -423,6 +448,8 @@ public static void createTablesIfNotExists(Start start, Connection con) throws S // index update(con, getQueryToCreateUserIdIndexForUsersTable(start), NO_OP_SETTER); + update(con, getQueryToCreateUserIdAppIdIndexForUsersTable(start), NO_OP_SETTER); + update(con, getQueryToCreateAppIdIndexForUsersTable(start), NO_OP_SETTER); update(con, getQueryToCreateTenantIdIndexForUsersTable(start), NO_OP_SETTER); } @@ -553,6 +580,37 @@ public static void createTablesIfNotExists(Start start, Connection con) throws S update(start, BulkImportQueries.getQueryToCreatePaginationIndex2(start), NO_OP_SETTER); } + if (!doesTableExists(start, con, Config.getConfig(start).getOAuthClientsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(con, OAuthQueries.getQueryToCreateOAuthClientTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, con, Config.getConfig(start).getOAuthSessionsTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(con, OAuthQueries.getQueryToCreateOAuthSessionsTable(start), NO_OP_SETTER); + + // index + update(con, OAuthQueries.getQueryToCreateOAuthSessionsExpIndex(start), NO_OP_SETTER); + update(con, OAuthQueries.getQueryToCreateOAuthSessionsExternalRefreshTokenIndex(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, con, Config.getConfig(start).getOAuthM2MTokensTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(con, OAuthQueries.getQueryToCreateOAuthM2MTokensTable(start), NO_OP_SETTER); + + // index + update(con, OAuthQueries.getQueryToCreateOAuthM2MTokenIatIndex(start), NO_OP_SETTER); + update(con, OAuthQueries.getQueryToCreateOAuthM2MTokenExpIndex(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, con, Config.getConfig(start).getOAuthLogoutChallengesTable())) { + getInstance(start).addState(CREATING_NEW_TABLE, null); + update(con, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTable(start), NO_OP_SETTER); + + // index + update(con, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(start), NO_OP_SETTER); + } + } catch (Exception e) { if (e.getMessage().contains("schema") && e.getMessage().contains("does not exist") && numberOfRetries < 1) { @@ -634,10 +692,18 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer + getConfig(start).getUserRolesTable() + "," + getConfig(start).getDashboardUsersTable() + "," + getConfig(start).getDashboardSessionsTable() + "," - + getConfig(start).getTotpUsedCodesTable() + "," + + getConfig(start).getOAuthClientsTable() + "," + + getConfig(start).getOAuthM2MTokensTable() + "," + + getConfig(start).getOAuthLogoutChallengesTable() + "," + + getConfig(start).getTotpUsedCodesTable() + "," + getConfig(start).getTotpUserDevicesTable() + "," + getConfig(start).getTotpUsersTable() + "," - + getConfig(start).getBulkImportUsersTable(); + + getConfig(start).getBulkImportUsersTable() + "," + + getConfig(start).getOAuthClientsTable() + "," + + getConfig(start).getOAuthSessionsTable() + "," + + getConfig(start).getOAuthLogoutChallengesTable() + "," + + getConfig(start).getOAuthM2MTokensTable(); + update(start, DROP_QUERY, NO_OP_SETTER); } } @@ -718,8 +784,8 @@ public static void deleteKeyValue_Transaction(Start start, Connection con, Tenan public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { StringBuilder QUERY = new StringBuilder( - "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " + - getConfig(start).getUsersTable()); + "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 ("); @@ -732,6 +798,7 @@ public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIP } 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()); @@ -752,7 +819,8 @@ public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIP public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { StringBuilder QUERY = new StringBuilder( - "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " + getConfig(start).getUsersTable()); + "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 ("); @@ -766,6 +834,8 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, 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()); @@ -795,7 +865,8 @@ public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, }, ResultSet::next); } - public static boolean doesUserIdExist_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + public static boolean doesUserIdExist_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { // We query both tables cause there is a case where a primary user ID exists, but its associated // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. @@ -825,6 +896,24 @@ public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdenti }, ResultSet::next); } + public static List findUserIdsThatExist(Start start, AppIdentifier appIdentifier, List userIds) + throws SQLException, StorageQueryException { + String QUERY = "SELECT user_id FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id IN ("+ Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +")"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for(int i = 0; i { + List foundUserIds = new ArrayList<>(); + while(result.next()){ + foundUserIds.add(result.getString(1)); + } + return foundUserIds; + }); + } + public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, @NotNull String timeJoinedOrder, @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @@ -1007,8 +1096,11 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant 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 "; + 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)); @@ -1044,9 +1136,12 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant 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 " + 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 = ?" + + + " ? 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 -> { @@ -1072,7 +1167,8 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant }); } 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 "; + 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"; } @@ -1143,6 +1239,37 @@ 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 { + + String users_update_QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; + String appid_to_userid_update_QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; + + PreparedStatement usersUpdateStatement = sqlCon.prepareStatement(users_update_QUERY); + PreparedStatement appIdToUserIdUpdateStatement = sqlCon.prepareStatement(appid_to_userid_update_QUERY); + int counter = 0; + for(String userId: userIds){ + usersUpdateStatement.setString(1, appIdentifier.getAppId()); + usersUpdateStatement.setString(2, userId); + usersUpdateStatement.addBatch(); + + appIdToUserIdUpdateStatement.setString(1, appIdentifier.getAppId()); + appIdToUserIdUpdateStatement.setString(2, userId); + appIdToUserIdUpdateStatement.addBatch(); + + counter++; + if(counter % 100 == 0) { + usersUpdateStatement.executeBatch(); + appIdToUserIdUpdateStatement.executeBatch(); + } + } + usersUpdateStatement.executeBatch(); + appIdToUserIdUpdateStatement.executeBatch(); + } + public static void linkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String recipeUserId, String primaryUserId) throws SQLException, StorageQueryException { @@ -1173,6 +1300,73 @@ public static void linkAccounts_Transaction(Start start, Connection sqlCon, AppI } } + public static void linkMultipleAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + Map recipeUserIdToPrimaryUserId) + throws SQLException, StorageQueryException { + + if(recipeUserIdToPrimaryUserId == null || recipeUserIdToPrimaryUserId.isEmpty()){ + return; + } + + 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 = ?"; + + String update_appid_to_userid_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 = ?"; + + PreparedStatement updateUsers = sqlCon.prepareStatement(update_users_QUERY); + PreparedStatement updateAppIdToUserId = sqlCon.prepareStatement(update_appid_to_userid_QUERY); + + int counter = 0; + for(Map.Entry linkEntry : recipeUserIdToPrimaryUserId.entrySet()) { + String primaryUserId = linkEntry.getValue(); + String recipeUserId = linkEntry.getKey(); + + updateUsers.setString(1, primaryUserId); + updateUsers.setString(2, appIdentifier.getAppId()); + updateUsers.setString(3, recipeUserId); + updateUsers.addBatch(); + + updateAppIdToUserId.setString(1, primaryUserId); + updateAppIdToUserId.setString(2, appIdentifier.getAppId()); + updateAppIdToUserId.setString(3, recipeUserId); + updateAppIdToUserId.addBatch(); + + counter++; + if (counter % 100 == 0) { + updateUsers.executeBatch(); + updateAppIdToUserId.executeBatch(); + } + } + + updateUsers.executeBatch(); + updateAppIdToUserId.executeBatch(); + + updateTimeJoinedForPrimaryUsers_Transaction(start, sqlCon, appIdentifier, + new ArrayList<>(recipeUserIdToPrimaryUserId.values())); + } + + public static void updateTimeJoinedForPrimaryUsers_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, List primaryUserIds) + throws SQLException, StorageQueryException { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " + + getConfig(start).getUsersTable() + " WHERE app_id = ? AND primary_or_recipe_user_id = ?) WHERE " + + " app_id = ? AND primary_or_recipe_user_id = ?"; + PreparedStatement updateStatement = sqlCon.prepareStatement(QUERY); + for(String primaryUserId : primaryUserIds) { + updateStatement.setString(1, appIdentifier.getAppId()); + updateStatement.setString(2, primaryUserId); + updateStatement.setString(3, appIdentifier.getAppId()); + updateStatement.setString(4, primaryUserId); + updateStatement.addBatch(); + } + + updateStatement.executeBatch(); + } + public static void unlinkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String primaryUserId, String recipeUserId) throws SQLException, StorageQueryException { @@ -1258,7 +1452,8 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction( // now that we have locks on all the relevant tables, we can read from them safely List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, thirdPartyId, thirdPartyUserId); - List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, userIds); + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, + userIds); // this is going to order them based on oldest that joined to newest that joined. result.sort(Comparator.comparingLong(o -> o.timeJoined)); @@ -1303,6 +1498,39 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(Start sta return result.toArray(new AuthRecipeUserInfo[0]); } + public static AuthRecipeUserInfo[] listPrimaryUsersByMultipleEmailsOrPhonesOrThirdParty_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List emails, List phones, + Map thirdpartyUserIdToThirdpartyId) + throws SQLException, StorageQueryException { + Set userIds = new HashSet<>(); + + //I am not really sure this is really needed.. + EmailPasswordQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, emails); + ThirdPartyQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, emails); + PasswordlessQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, emails); + PasswordlessQueries.lockPhoneAndTenant_Transaction(start, sqlCon, appIdentifier, phones); + ThirdPartyQueries.lockThirdPartyInfoAndTenant_Transaction(start, sqlCon, appIdentifier, thirdpartyUserIdToThirdpartyId); + + //collect ids by email + userIds.addAll(EmailPasswordQueries.getPrimaryUserIdsUsingMultipleEmails_Transaction(start, sqlCon, appIdentifier, + emails)); + userIds.addAll(PasswordlessQueries.getPrimaryUserIdsUsingMultipleEmails_Transaction(start, sqlCon, appIdentifier, + emails)); + userIds.addAll(ThirdPartyQueries.getPrimaryUserIdsUsingMultipleEmails_Transaction(start, sqlCon, appIdentifier, emails)); + + //collect ids by phone + userIds.addAll(PasswordlessQueries.listUserIdsByMultiplePhoneNumber_Transaction(start, sqlCon, appIdentifier, phones)); + + //collect ids by thirdparty + userIds.addAll(ThirdPartyQueries.listUserIdsByMultipleThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, thirdpartyUserIdToThirdpartyId)); + + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, + new ArrayList<>(userIds)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { @@ -1356,9 +1584,9 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(Start start, } public static AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(Start start, - TenantIdentifier tenantIdentifier, - String thirdPartyId, - String thirdPartyUserId) + TenantIdentifier tenantIdentifier, + String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException, SQLException { String userId = ThirdPartyQueries.getUserIdByThirdPartyInfo(start, tenantIdentifier, thirdPartyId, thirdPartyUserId); @@ -1392,7 +1620,7 @@ public static AuthRecipeUserInfo getPrimaryUserInfoForUserId(Start start, AppIde } public static AuthRecipeUserInfo getPrimaryUserInfoForUserId_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String id) + AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { List ids = new ArrayList<>(); ids.add(id); @@ -1403,6 +1631,17 @@ public static AuthRecipeUserInfo getPrimaryUserInfoForUserId_Transaction(Start s return result.get(0); } + public static List getPrimaryUserInfosForUserIds_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, List ids) + throws SQLException, StorageQueryException { + + List result = getPrimaryUserInfoForUserIds_Transaction(start, con, appIdentifier, ids); + if (result.isEmpty()) { + return null; + } + return result; + } + private static List getPrimaryUserInfoForUserIds(Start start, AppIdentifier appIdentifier, List userIds) @@ -1415,14 +1654,18 @@ private static List getPrimaryUserInfoForUserIds(Start start // 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 = ?"; + 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 @@ -1496,8 +1739,8 @@ private static List getPrimaryUserInfoForUserIds(Start start } private static List getPrimaryUserInfoForUserIds_Transaction(Start start, Connection sqlCon, - AppIdentifier appIdentifier, - List userIds) + AppIdentifier appIdentifier, + List userIds) throws StorageQueryException, SQLException { if (userIds.size() == 0) { return new ArrayList<>(); @@ -1507,14 +1750,20 @@ 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 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 = ?"; + 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(sqlCon, QUERY, pst -> { // IN user_id @@ -1528,7 +1777,8 @@ private static List getPrimaryUserInfoForUserIds_Transaction } // for app_id pst.setString(index, appIdentifier.getAppId()); - pst.setString(index + 1, appIdentifier.getAppId()); + pst.setString(index+1, appIdentifier.getAppId()); +// System.out.println(pst); }, result -> { List parsedResult = new ArrayList<>(); while (result.next()) { @@ -1551,10 +1801,13 @@ private static List getPrimaryUserInfoForUserIds_Transaction List loginMethods = new ArrayList<>(); loginMethods.addAll( - EmailPasswordQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); - loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + EmailPasswordQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, + appIdentifier)); + loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, + appIdentifier)); loginMethods.addAll( - PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, + appIdentifier)); Map recipeUserIdToLoginMethodMap = new HashMap<>(); for (LoginMethod loginMethod : loginMethods) { @@ -1753,20 +2006,20 @@ public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdenti public static int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String QUERY = - "SELECT COUNT (DISTINCT user_id) as c FROM (" - + " (" // Users with number of login methods > 1 - + " SELECT primary_or_recipe_user_id AS user_id FROM (" - + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" - + " FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? " - + " GROUP BY (app_id, primary_or_recipe_user_id)" - + " ) AS nloginmethods" - + " WHERE num_login_methods > 1" - + " ) UNION (" // TOTP users - + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() - + " WHERE app_id = ?" - + " )" - + ") AS all_users"; + "SELECT COUNT (DISTINCT user_id) as c FROM (" + + " (" // Users with number of login methods > 1 + + " SELECT primary_or_recipe_user_id AS user_id FROM (" + + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? " + + " GROUP BY (app_id, primary_or_recipe_user_id)" + + " ) AS nloginmethods" + + " WHERE num_login_methods > 1" + + " ) UNION (" // TOTP users + + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() + + " WHERE app_id = ?" + + " )" + + ") AS all_users"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -1789,7 +2042,8 @@ public static boolean checkIfUsesAccountLinking(Start start, AppIdentifier appId }); } - public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { GeneralQueries.AccountLinkingInfo accountLinkingInfo = new GeneralQueries.AccountLinkingInfo(userId, false); { @@ -1811,7 +2065,8 @@ public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, return accountLinkingInfo; } - public static void updateTimeJoinedForPrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String primaryUserId) + public static void updateTimeJoinedForPrimaryUser_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, String primaryUserId) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getUsersTable() + " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " + diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java index f57c4402..9e80fe48 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/JWTSigningQueries.java @@ -52,14 +52,14 @@ static String getQueryToCreateJWTSigningTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + jwtSigningKeysTable + " (" + "app_id VARCHAR(64) DEFAULT 'public'," + "key_id VARCHAR(255) NOT NULL," - + "key_string TEXT NOT NULL," + + "key_string TEXT NOT NULL," + "algorithm VARCHAR(10) NOT NULL," - + "created_at BIGINT," + + "created_at BIGINT," + "CONSTRAINT " + Utils.getConstraintName(schema, jwtSigningKeysTable, null, "pkey") + " PRIMARY KEY(app_id, key_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, jwtSigningKeysTable, "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -75,7 +75,7 @@ public static List getJWTSigningKeys_Transaction(Start start, String QUERY = "SELECT * FROM " + getConfig(start).getJWTSigningKeysTable() + " WHERE app_id = ? ORDER BY created_at DESC FOR UPDATE"; - return execute(con, QUERY, pst -> pst.setString(1, appIdentifier.getAppId()),result -> { + return execute(con, QUERY, pst -> pst.setString(1, appIdentifier.getAppId()), result -> { List keys = new ArrayList<>(); while (result.next()) { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index 0d9bf826..b0c7ffe1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -18,7 +18,9 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; @@ -32,8 +34,8 @@ import java.sql.SQLException; import java.util.HashMap; -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import static io.supertokens.storage.postgresql.config.Config.getConfig; public class MultitenancyQueries { @@ -51,7 +53,9 @@ static String getQueryToCreateTenantConfigsTable(Start start) { + "email_password_enabled BOOLEAN," + "passwordless_enabled BOOLEAN," + "third_party_enabled BOOLEAN," - + "CONSTRAINT " + Utils.getConstraintName(schema, tenantConfigsTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id)" + + "is_first_factors_null BOOLEAN," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantConfigsTable, null, "pkey") + + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id)" + ");"; // @formatter:on } @@ -82,10 +86,12 @@ static String getQueryToCreateTenantThirdPartyProvidersTable(Start start) { + "user_info_map_from_user_info_endpoint_user_id VARCHAR(64)," + "user_info_map_from_user_info_endpoint_email VARCHAR(64)," + "user_info_map_from_user_info_endpoint_email_verified VARCHAR(64)," - + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, null, "pkey") + + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, "tenant_id", "fkey") + " FOREIGN KEY(connection_uri_domain, app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -109,16 +115,20 @@ static String getQueryToCreateTenantThirdPartyProviderClientsTable(Start start) + "scope VARCHAR(128)[]," + "force_pkce BOOLEAN," + "additional_config TEXT," - + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, null, "pkey") + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type)," - + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, "third_party_id", "fkey") + + "CONSTRAINT " + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, null, "pkey") + + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type)," + + "CONSTRAINT " + + Utils.getConstraintName(schema, tenantThirdPartyProvidersTable, "third_party_id", "fkey") + " FOREIGN KEY(connection_uri_domain, app_id, tenant_id, third_party_id)" - + " REFERENCES " + Config.getConfig(start).getTenantThirdPartyProvidersTable() + " (connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getTenantThirdPartyProvidersTable() + + " (connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE" + ");"; } public static String getQueryToCreateThirdPartyIdIndexForTenantThirdPartyProviderClientsTable(Start start) { return "CREATE INDEX IF NOT EXISTS tenant_thirdparty_provider_clients_third_party_id_index ON " - + getConfig(start).getTenantThirdPartyProviderClientsTable() + " (connection_uri_domain, app_id, tenant_id, third_party_id);"; + + getConfig(start).getTenantThirdPartyProviderClientsTable() + + " (connection_uri_domain, app_id, tenant_id, third_party_id);"; } public static String getQueryToCreateFirstFactorsTable(Start start) { @@ -134,7 +144,8 @@ public static String getQueryToCreateFirstFactorsTable(Start start) { + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + " FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -157,14 +168,16 @@ public static String getQueryToCreateRequiredSecondaryFactorsTable(Start start) + " PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "tenant_id", "fkey") + " FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + ");"; // @formatter:on } public static String getQueryToCreateTenantIdIndexForRequiredSecondaryFactorsTable(Start start) { return "CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON " - + getConfig(start).getTenantRequiredSecondaryFactorsTable() + " (connection_uri_domain, app_id, tenant_id);"; + + getConfig(start).getTenantRequiredSecondaryFactorsTable() + + " (connection_uri_domain, app_id, tenant_id);"; } @@ -173,19 +186,23 @@ private static void executeCreateTenantQueries(Start start, Connection sqlCon, T TenantConfigSQLHelper.create(start, sqlCon, tenantConfig); - for (ThirdPartyConfig.Provider provider : tenantConfig.thirdPartyConfig.providers) { - ThirdPartyProviderSQLHelper.create(start, sqlCon, tenantConfig, provider); + if (tenantConfig.thirdPartyConfig.providers != null) { + for (ThirdPartyConfig.Provider provider : tenantConfig.thirdPartyConfig.providers) { + ThirdPartyProviderSQLHelper.create(start, sqlCon, tenantConfig, provider); - for (ThirdPartyConfig.ProviderClient providerClient : provider.clients) { - ThirdPartyProviderClientSQLHelper.create(start, sqlCon, tenantConfig, provider, providerClient); + for (ThirdPartyConfig.ProviderClient providerClient : provider.clients) { + ThirdPartyProviderClientSQLHelper.create(start, sqlCon, tenantConfig, provider, providerClient); + } } } MfaSqlHelper.createFirstFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.firstFactors); - MfaSqlHelper.createRequiredSecondaryFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.requiredSecondaryFactors); + MfaSqlHelper.createRequiredSecondaryFactors(start, sqlCon, tenantConfig.tenantIdentifier, + tenantConfig.requiredSecondaryFactors); } - public static void createTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { + public static void createTenantConfig(Start start, TenantConfig tenantConfig) + throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); { @@ -201,7 +218,8 @@ public static void createTenantConfig(Start start, TenantConfig tenantConfig) th }); } - public static boolean deleteTenantConfig(Start start, TenantIdentifier tenantIdentifier) throws StorageQueryException { + public static boolean deleteTenantConfig(Start start, TenantIdentifier tenantIdentifier) + throws StorageQueryException { try { String QUERY = "DELETE FROM " + getConfig(start).getTenantConfigsTable() + " WHERE connection_uri_domain = ? AND app_id = ? AND tenant_id = ?"; @@ -219,7 +237,8 @@ public static boolean deleteTenantConfig(Start start, TenantIdentifier tenantIde } } - public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { + public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig) + throws StorageQueryException, StorageTransactionLogicException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); { @@ -233,7 +252,8 @@ public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig) pst.setString(3, tenantConfig.tenantIdentifier.getTenantId()); }); if (rowsAffected == 0) { - throw new StorageTransactionLogicException(new TenantOrAppNotFoundException(tenantConfig.tenantIdentifier)); + throw new StorageTransactionLogicException( + new TenantOrAppNotFoundException(tenantConfig.tenantIdentifier)); } } @@ -256,16 +276,21 @@ public static TenantConfig[] getAllTenants(Start start) throws StorageQueryExcep try { // Map TenantIdentifier -> thirdPartyId -> clientType - HashMap>> providerClientsMap = ThirdPartyProviderClientSQLHelper.selectAll(start); + HashMap>> providerClientsMap = ThirdPartyProviderClientSQLHelper.selectAll( + start); // Map (tenantIdentifier) -> thirdPartyId -> provider - HashMap> providerMap = ThirdPartyProviderSQLHelper.selectAll(start, providerClientsMap); + HashMap> providerMap = + ThirdPartyProviderSQLHelper.selectAll( + start, providerClientsMap); // Map (tenantIdentifier) -> firstFactors HashMap firstFactorsMap = MfaSqlHelper.selectAllFirstFactors(start); // Map (tenantIdentifier) -> requiredSecondaryFactors - HashMap requiredSecondaryFactorsMap = MfaSqlHelper.selectAllRequiredSecondaryFactors(start); + HashMap requiredSecondaryFactorsMap = + MfaSqlHelper.selectAllRequiredSecondaryFactors( + start); return TenantConfigSQLHelper.selectAll(start, providerMap, firstFactorsMap, requiredSecondaryFactorsMap); } catch (SQLException throwables) { @@ -290,7 +315,7 @@ public static void addTenantIdInTargetStorage(Start start, TenantIdentifier tena } public static void addTenantIdInTargetStorage_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier) throws + TenantIdentifier tenantIdentifier) throws SQLException, StorageQueryException { { if (Start.isTesting && simulateErrorInAddingTenantIdInTargetStorage_forTesting) { @@ -314,7 +339,7 @@ public static void addTenantIdInTargetStorage_Transaction(Start start, Connectio + "(app_id, created_at_time)" + " VALUES(?, ?) ON CONFLICT DO NOTHING"; update(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); - pst.setLong(2, currentTime); + pst.setLong(2, currentTime); }); } @@ -325,14 +350,14 @@ public static void addTenantIdInTargetStorage_Transaction(Start start, Connectio update(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); - pst.setLong(3, currentTime); + pst.setLong(3, currentTime); }); } } } public static void deleteTenantIdInTargetStorage(Start start, TenantIdentifier tenantIdentifier) - throws StorageQueryException { + throws StorageQueryException { try { if (tenantIdentifier.getTenantId().equals(TenantIdentifier.DEFAULT_TENANT_ID)) { // Delete the app diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java new file mode 100644 index 00000000..c6f59566 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.storage.postgresql.queries; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.oauth.OAuthClient; +import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.Config; +import io.supertokens.storage.postgresql.utils.Utils; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; + + +public class OAuthQueries { + + public static String getQueryToCreateOAuthClientTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String oAuth2ClientTable = Config.getConfig(start).getOAuthClientsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2ClientTable + " (" + + "app_id VARCHAR(64)," + + "client_id VARCHAR(255) NOT NULL," + + "client_secret TEXT," + + "enable_refresh_token_rotation BOOLEAN NOT NULL," + + "is_client_credentials_only BOOLEAN NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuth2ClientTable, null, "pkey") + + " PRIMARY KEY (app_id, client_id)," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuth2ClientTable, "app_id", "fkey") + + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE " + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthSessionsTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String oAuthSessionsTable = Config.getConfig(start).getOAuthSessionsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuthSessionsTable + " (" + + "gid VARCHAR(255)," // needed for instrospect. It's much easier to find these records if we have a gid + + "app_id VARCHAR(64) DEFAULT 'public'," + + "client_id VARCHAR(255) NOT NULL," + + "session_handle VARCHAR(128)," + + "external_refresh_token VARCHAR(255) UNIQUE," + + "internal_refresh_token VARCHAR(255) UNIQUE," + + "jti TEXT NOT NULL," // comma separated jti list + + "exp BIGINT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuthSessionsTable, null, "pkey") + + " PRIMARY KEY (gid)," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuthSessionsTable, "client_id", "fkey") + + " FOREIGN KEY(app_id, client_id) REFERENCES " + Config.getConfig(start).getOAuthClientsTable() + "(app_id, client_id) ON DELETE CASCADE);"; + // @formatter:on + } + + public static String getQueryToCreateOAuthSessionsExpIndex(Start start) { + String oAuth2SessionTable = Config.getConfig(start).getOAuthSessionsTable(); + return "CREATE INDEX IF NOT EXISTS oauth_session_exp_index ON " + + oAuth2SessionTable + "(exp DESC);"; + } + + public static String getQueryToCreateOAuthSessionsExternalRefreshTokenIndex(Start start) { + String oAuth2SessionTable = Config.getConfig(start).getOAuthSessionsTable(); + return "CREATE INDEX IF NOT EXISTS oauth_session_external_refresh_token_index ON " + + oAuth2SessionTable + "(app_id, external_refresh_token DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokensTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2M2MTokensTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "client_id VARCHAR(255) NOT NULL," + + "iat BIGINT NOT NULL," + + "exp BIGINT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuth2M2MTokensTable, null, "pkey") + + " PRIMARY KEY (app_id, client_id, iat)," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuth2M2MTokensTable, "client_id", "fkey") + + " FOREIGN KEY(app_id, client_id)" + + " REFERENCES " + Config.getConfig(start).getOAuthClientsTable() + "(app_id, client_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthM2MTokenIatIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_iat_index ON " + + oAuth2M2MTokensTable + "(iat DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokenExpIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_exp_index ON " + + oAuth2M2MTokensTable + "(exp DESC);"; + } + + public static String getQueryToCreateOAuthLogoutChallengesTable(Start start) { + String schema = Config.getConfig(start).getTableSchema(); + String oAuth2LogoutChallengesTable = Config.getConfig(start).getOAuthLogoutChallengesTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2LogoutChallengesTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "challenge VARCHAR(128) NOT NULL," + + "client_id VARCHAR(255) NOT NULL," + + "post_logout_redirect_uri VARCHAR(1024)," + + "session_handle VARCHAR(128)," + + "state VARCHAR(128)," + + "time_created BIGINT NOT NULL," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuth2LogoutChallengesTable, null, "pkey") + + " PRIMARY KEY (app_id, challenge)," + + "CONSTRAINT " + Utils.getConstraintName(schema, oAuth2LogoutChallengesTable, "client_id", "fkey") + + " FOREIGN KEY(app_id, client_id)" + + " REFERENCES " + Config.getConfig(start).getOAuthClientsTable() + "(app_id, client_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(Start start) { + String oAuth2LogoutChallengesTable = Config.getConfig(start).getOAuthLogoutChallengesTable(); + return "CREATE INDEX IF NOT EXISTS oauth_logout_challenges_time_created_index ON " + + oAuth2LogoutChallengesTable + "(time_created DESC);"; + } + + public static OAuthClient getOAuthClientById(Start start, String clientId, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT client_secret, is_client_credentials_only, enable_refresh_token_rotation FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE client_id = ? AND app_id = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, clientId); + pst.setString(2, appIdentifier.getAppId()); + }, (result) -> { + if (result.next()) { + return new OAuthClient(clientId, result.getString("client_secret"), result.getBoolean("is_client_credentials_only"), result.getBoolean("enable_refresh_token_rotation")); + } + return null; + }); + } + + public static void createOrUpdateOAuthSession(Start start, AppIdentifier appIdentifier, @NotNull String gid, @NotNull String clientId, + String externalRefreshToken, String internalRefreshToken, String sessionHandle, + String jti, long exp) + throws SQLException, StorageQueryException { + String sessionTable = Config.getConfig(start).getOAuthSessionsTable(); + String QUERY = "INSERT INTO " + sessionTable + + " (gid, client_id, app_id, external_refresh_token, internal_refresh_token, session_handle, jti, exp) VALUES (?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (gid) DO UPDATE SET external_refresh_token = ?, internal_refresh_token = ?, " + + "session_handle = ? , jti = CONCAT("+sessionTable+".jti, ?), exp = ?"; + update(start, QUERY, pst -> { + String jtiToInsert = jti + ","; + + pst.setString(1, gid); + pst.setString(2, clientId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, externalRefreshToken); + pst.setString(5, internalRefreshToken); + pst.setString(6, sessionHandle); + pst.setString(7, jtiToInsert); //the starting list element also has to have a "," at the end as the remove removes "jti + ," + pst.setLong(8, exp); + + pst.setString(9, externalRefreshToken); + pst.setString(10, internalRefreshToken); + pst.setString(11, sessionHandle); + pst.setString(12, jtiToInsert); + pst.setLong(13, exp); + }); + } + + public static List getOAuthClients(Start start, AppIdentifier appIdentifier, List clientIds) + throws SQLException, StorageQueryException { + if(clientIds.isEmpty()){ + return Collections.emptyList(); + } + String QUERY = "SELECT * FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND client_id IN ( " + + Utils.generateCommaSeperatedQuestionMarks(clientIds.size()) + + " );"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < clientIds.size(); i++) { + pst.setString(i + 2, clientIds.get(i)); + } + }, (result) -> { + List res = new ArrayList<>(); + while (result.next()) { + res.add(new OAuthClient(result.getString("client_id"), result.getString("client_secret"), result.getBoolean("is_client_credentials_only"), result.getBoolean("enable_refresh_token_rotation"))); + } + return res; + }); + } + + public static void addOrUpdateOauthClient(Start start, AppIdentifier appIdentifier, String clientId, String clientSecret, + boolean isClientCredentialsOnly, boolean enableRefreshTokenRotation) + throws SQLException, StorageQueryException { + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientsTable() + + "(app_id, client_id, client_secret, is_client_credentials_only, enable_refresh_token_rotation) VALUES(?, ?, ?, ?, ?) " + + "ON CONFLICT (app_id, client_id) DO UPDATE SET client_secret = ?, is_client_credentials_only = ?, enable_refresh_token_rotation = ?"; + update(start, INSERT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + pst.setString(3, clientSecret); + pst.setBoolean(4, isClientCredentialsOnly); + pst.setBoolean(5, enableRefreshTokenRotation); + pst.setString(6, clientSecret); + pst.setBoolean(7, isClientCredentialsOnly); + pst.setBoolean(8, enableRefreshTokenRotation); + }); + } + + public static boolean deleteOAuthClient(Start start, String clientId, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND client_id = ?"; + int numberOfRow = update(start, DELETE, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + }); + return numberOfRow > 0; + } + + public static boolean deleteOAuthSessionByGID(Start start, AppIdentifier appIdentifier, String gid) + throws SQLException, StorageQueryException { + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE gid = ? and app_id = ?;"; + int numberOfRows = update(start, DELETE, pst -> { + pst.setString(1, gid); + pst.setString(2, appIdentifier.getAppId()); + }); + return numberOfRows > 0; + } + + public static boolean deleteOAuthSessionByClientId(Start start, AppIdentifier appIdentifier, String clientId) + throws SQLException, StorageQueryException { + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and client_id = ?;"; + int numberOfRows = update(start, DELETE, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + }); + return numberOfRows > 0; + } + + public static boolean deleteOAuthSessionBySessionHandle(Start start, AppIdentifier appIdentifier, String sessionHandle) + throws SQLException, StorageQueryException { + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and session_handle = ?"; + int numberOfRows = update(start, DELETE, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, sessionHandle); + }); + return numberOfRows > 0; + } + + public static boolean deleteJTIFromOAuthSession(Start start, AppIdentifier appIdentifier, String gid, String jti) + throws SQLException, StorageQueryException { + //jti is a comma separated list. When deleting a jti, just have to delete from the list + String DELETE = "UPDATE " + Config.getConfig(start).getOAuthSessionsTable() + + " SET jti = REPLACE(jti, ?, '')" // deletion means replacing the jti with empty char + + " WHERE app_id = ? and gid = ?"; + int numberOfRows = update(start, DELETE, pst -> { + pst.setString(1, jti + ","); //removing with the "," to not leave behind trash + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, gid); + }); + return numberOfRows > 0; + } + + public static int countTotalNumberOfClients(Start start, AppIdentifier appIdentifier, + boolean filterByClientCredentialsOnly) throws SQLException, StorageQueryException { + if (filterByClientCredentialsOnly) { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND is_client_credentials_only = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setBoolean(2, true); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } else { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + } + + public static int countTotalNumberOfOAuthM2MTokensAlive(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp > ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, System.currentTimeMillis()/1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static int countTotalNumberOfOAuthM2MTokensCreatedSince(Start start, AppIdentifier appIdentifier, long since) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND iat >= ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, since / 1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static void addOAuthM2MTokenForStats(Start start, AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthM2MTokensTable() + + " (app_id, client_id, iat, exp) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + pst.setLong(3, iat); + pst.setLong(4, exp); + }); + } + + public static void addOAuthLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge, String clientId, + String postLogoutRedirectionUri, String sessionHandle, String state, long timeCreated) throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " (app_id, challenge, client_id, post_logout_redirect_uri, session_handle, state, time_created) VALUES (?, ?, ?, ?, ?, ?, ?)"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, challenge); + pst.setString(3, clientId); + pst.setString(4, postLogoutRedirectionUri); + pst.setString(5, sessionHandle); + pst.setString(6, state); + pst.setLong(7, timeCreated); + }); + } + + public static OAuthLogoutChallenge getOAuthLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge) throws SQLException, StorageQueryException { + String QUERY = "SELECT challenge, client_id, post_logout_redirect_uri, session_handle, state, time_created FROM " + + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " WHERE app_id = ? AND challenge = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, challenge); + }, result -> { + if (result.next()) { + return new OAuthLogoutChallenge( + result.getString("challenge"), + result.getString("client_id"), + result.getString("post_logout_redirect_uri"), + result.getString("session_handle"), + result.getString("state"), + result.getLong("time_created") + ); + } + return null; + }); + } + + public static void deleteOAuthLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " WHERE app_id = ? AND challenge = ?"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, challenge); + }); + } + + public static void deleteOAuthLogoutChallengesBefore(Start start, long time) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " WHERE time_created < ?"; + update(start, QUERY, pst -> { + pst.setLong(1, time); + }); + } + + public static String getRefreshTokenMapping(Start start, AppIdentifier appIdentifier, String externalRefreshToken) throws SQLException, StorageQueryException { + String QUERY = "SELECT internal_refresh_token FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? AND external_refresh_token = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, externalRefreshToken); + }, result -> { + if (result.next()) { + return result.getString("internal_refresh_token"); + } + return null; + }); + } + + public static void deleteExpiredOAuthSessions(Start start, long exp) throws SQLException, StorageQueryException { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE exp < ?"; + + update(start, QUERY, pst -> { + pst.setLong(1, exp); + }); + } + + public static void deleteExpiredOAuthM2MTokens(Start start, long exp) throws SQLException, StorageQueryException { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE exp < ?"; + update(start, QUERY, pst -> { + pst.setLong(1, exp); + }); + } + + public static boolean isOAuthSessionExistsByJTI(Start start, AppIdentifier appIdentifier, String gid, String jti) + throws SQLException, StorageQueryException { + String SELECT = "SELECT jti FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and gid = ?;"; + return execute(start, SELECT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, gid); + }, result -> { + if(result.next()){ + List jtis = Arrays.stream(result.getString(1).split(",")).filter(s -> !s.isEmpty()).collect( + Collectors.toList()); + return jtis.contains(jti); + } + return false; + }); + } + + public static boolean isOAuthSessionExistsByGID(Start start, AppIdentifier appIdentifier, String gid) + throws SQLException, StorageQueryException { + String SELECT = "SELECT count(*) FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and gid = ?;"; + return execute(start, SELECT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, gid); + }, result -> { + if(result.next()){ + return result.getInt(1) > 0; + } + return false; + }); + } + +} 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 8f8df3d6..6d3089cd 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/PasswordlessQueries.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; +import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser; import io.supertokens.pluginInterface.sqlStorage.SQLStorage.TransactionIsolationLevel; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; @@ -35,6 +36,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -388,7 +390,8 @@ public static void deleteCode_Transaction(Start start, Connection con, TenantIde }); } - public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, + public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, + @Nullable String email, @Nullable String phoneNumber, long timeJoined) throws StorageTransactionLogicException, StorageQueryException { return start.startTransaction(con -> { @@ -407,7 +410,9 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant { // 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, primary_or_recipe_user_time_joined)" + + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + + "primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -457,7 +462,7 @@ public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenant } private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String userId) + 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 " @@ -742,7 +747,7 @@ public static List getUsersInfoUsingIdList(Start start, Set } public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, - AppIdentifier appIdentifier) + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { if (ids.size() > 0) { // No need to filter based on tenantId because the id list is already filtered for a tenant @@ -772,7 +777,7 @@ public static List getUsersInfoUsingIdList_Transaction(Start start, } private static UserInfoPartial getUserById_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, - String userId) + String userId) throws StorageQueryException, SQLException { // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id // table @@ -809,9 +814,33 @@ public static List lockEmail_Transaction(Start start, Connection con, Ap }); } + public static List lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List emails) + throws StorageQueryException, SQLException { + if(emails == null || emails.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ") FOR UPDATE"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < emails.size(); i++) { + pst.setString(2 + i, emails.get(i)); + } + }, result -> { + List results = new ArrayList<>(); + while (result.next()) { + results.add(result.getString("user_id")); + } + return results; + }); + } + public static List lockPhoneAndTenant_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, - String phoneNumber) + AppIdentifier appIdentifier, + String phoneNumber) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + @@ -828,8 +857,32 @@ public static List lockPhoneAndTenant_Transaction(Start start, Connectio }); } + public static List lockPhoneAndTenant_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List phones) + throws StorageQueryException, SQLException { + if(phones == null || phones.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND phone_number IN (" + Utils.generateCommaSeperatedQuestionMarks(phones.size()) + ") FOR UPDATE"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < phones.size(); i++) { + pst.setString(2 + i, phones.get(i)); + } + }, result -> { + List results = new ArrayList<>(); + while (result.next()) { + results.add(result.getString("user_id")); + } + return results; + }); + } + public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, - String email) + 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" + @@ -849,8 +902,9 @@ public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier te }); } - public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, - String email) + public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String email) throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + @@ -870,6 +924,33 @@ public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, }); } + public static List getPrimaryUserIdsUsingMultipleEmails_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List emails) + throws StorageQueryException, SQLException { + if(emails.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS ep" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.email IN ( " + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + " )"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < emails.size(); i++) { + pst.setString(2+i, emails.get(i)); + } + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + public static String getPrimaryUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) throws StorageQueryException, SQLException { @@ -891,8 +972,9 @@ public static String getPrimaryUserByPhoneNumber(Start start, TenantIdentifier t }); } - public static List listUserIdsByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, - @Nonnull String phoneNumber) + public static List listUserIdsByPhoneNumber_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + @Nonnull String phoneNumber) throws StorageQueryException, SQLException { String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + @@ -912,6 +994,33 @@ public static List listUserIdsByPhoneNumber_Transaction(Start start, Con }); } + public static List listUserIdsByMultiplePhoneNumber_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + @Nonnull List phoneNumbers) + throws StorageQueryException, SQLException { + if(phoneNumbers.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.phone_number IN ( "+ Utils.generateCommaSeperatedQuestionMarks(phoneNumbers.size()) +" )"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < phoneNumbers.size(); i++) { + pst.setString(2 + i, phoneNumbers.get(i)); + } + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException, SQLException, UnknownUserIdException { @@ -922,11 +1031,14 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC throw new UnknownUserIdException(); } - GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, + sqlCon, tenantIdentifier.toAppIdentifier(), userId); { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, " + + "recipe_id, time_joined, primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -939,14 +1051,16 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC pst.setLong(8, userInfo.timeJoined); }); - GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), accountLinkingInfo.primaryUserId); + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), + accountLinkingInfo.primaryUserId); } { // passwordless_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + "(app_id, tenant_id, user_id, email, phone_number)" + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT ON CONSTRAINT " - + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getPasswordlessUserToTenantTable(), null, "pkey") + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), + getConfig(start).getPasswordlessUserToTenantTable(), null, "pkey") + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { @@ -1100,6 +1214,76 @@ private static List fillUserInfoWithTenantIds(Start start, return userInfos; } + public static void importUsers_Transaction(Connection sqlCon, Start start, + Collection users) throws SQLException { + + String app_id_to_user_id_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; + PreparedStatement appIdToUserIdStatement = sqlCon.prepareStatement(app_id_to_user_id_QUERY); + + String all_auth_recipe_users_QUERY = "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(?, ?, ?, ?, ?, ?, ?)"; + PreparedStatement allAuthRecipeUsersStatement = sqlCon.prepareStatement(all_auth_recipe_users_QUERY); + + String passwordless_users_QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUsersTable() + + "(app_id, user_id, email, phone_number, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + PreparedStatement passwordlessUsersStatement = sqlCon.prepareStatement(passwordless_users_QUERY); + + String passwordless_user_to_tenant_QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + + "(app_id, tenant_id, user_id, email, phone_number)" + " VALUES(?, ?, ?, ?, ?)"; + PreparedStatement passwordlessUserToTenantStatement = sqlCon.prepareStatement(passwordless_user_to_tenant_QUERY); + + int counter = 0; + for (PasswordlessImportUser user: users){ + TenantIdentifier tenantIdentifier = user.tenantIdentifier; + appIdToUserIdStatement.setString(1, tenantIdentifier.getAppId()); + appIdToUserIdStatement.setString(2, user.userId); + appIdToUserIdStatement.setString(3, user.userId); + appIdToUserIdStatement.setString(4, PASSWORDLESS.toString()); + appIdToUserIdStatement.addBatch(); + + allAuthRecipeUsersStatement.setString(1, tenantIdentifier.getAppId()); + allAuthRecipeUsersStatement.setString(2, tenantIdentifier.getTenantId()); + allAuthRecipeUsersStatement.setString(3, user.userId); + allAuthRecipeUsersStatement.setString(4, user.userId); + allAuthRecipeUsersStatement.setString(5, PASSWORDLESS.toString()); + allAuthRecipeUsersStatement.setLong(6, user.timeJoinedMSSinceEpoch); + allAuthRecipeUsersStatement.setLong(7, user.timeJoinedMSSinceEpoch); + allAuthRecipeUsersStatement.addBatch(); + + passwordlessUsersStatement.setString(1, tenantIdentifier.getAppId()); + passwordlessUsersStatement.setString(2, user.userId); + passwordlessUsersStatement.setString(3, user.email); + passwordlessUsersStatement.setString(4, user.phoneNumber); + passwordlessUsersStatement.setLong(5, user.timeJoinedMSSinceEpoch); + passwordlessUsersStatement.addBatch(); + + passwordlessUserToTenantStatement.setString(1, tenantIdentifier.getAppId()); + passwordlessUserToTenantStatement.setString(2, tenantIdentifier.getTenantId()); + passwordlessUserToTenantStatement.setString(3, user.userId); + passwordlessUserToTenantStatement.setString(4, user.email); + passwordlessUserToTenantStatement.setString(5, user.phoneNumber); + passwordlessUserToTenantStatement.addBatch(); + + counter++; + + if(counter % 100 == 0) { + appIdToUserIdStatement.executeBatch(); + allAuthRecipeUsersStatement.executeBatch(); + passwordlessUsersStatement.executeBatch(); + passwordlessUserToTenantStatement.executeBatch(); + } + } + + appIdToUserIdStatement.executeBatch(); + allAuthRecipeUsersStatement.executeBatch(); + passwordlessUsersStatement.executeBatch(); + passwordlessUserToTenantStatement.executeBatch(); + + } + private static class PasswordlessDeviceRowMapper implements RowMapper { private static final PasswordlessDeviceRowMapper INSTANCE = new PasswordlessDeviceRowMapper(); 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 0fe56e4d..c5108fd4 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/SessionQueries.java @@ -33,7 +33,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -306,6 +308,31 @@ public static String[] getAllNonExpiredSessionHandlesForUser(Start start, AppIde }); } + public static Map> getAllNonExpiredSessionHandlesForUsers(Start start, AppIdentifier appIdentifier, + List userIds) + throws SQLException, StorageQueryException { + String QUERY = "SELECT user_id, session_handle FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND expires_at >= ? AND user_id IN ( " + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + " )"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, currentTimeMillis()); + for(int i = 0; i < userIds.size() ; i++){ + pst.setString(3 + i, userIds.get(i)); + } + }, result -> { + Map> temp = new HashMap<>(); + while (result.next()) { + String userId = result.getString("user_id"); + if(!temp.containsKey(userId)){ + temp.put(userId, new ArrayList<>()); + } + temp.get(userId).add(result.getString("session_handle")); + } + return temp; + }); + } + public static void deleteAllExpiredSessions(Start start) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + " WHERE expires_at <= ?"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java index 8a8269d5..c14aa15d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/TOTPQueries.java @@ -1,22 +1,24 @@ package io.supertokens.storage.postgresql.queries; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.storage.postgresql.Start; -import io.supertokens.storage.postgresql.config.Config; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; +import io.supertokens.storage.postgresql.Start; +import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -32,7 +34,7 @@ public static String getQueryToCreateUsersTable(Start start) { + " PRIMARY KEY (app_id, user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") + " FOREIGN KEY(app_id)" - + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -119,10 +121,13 @@ private static int insertUser_Transaction(Start start, Connection con, AppIdenti }); } - private static int insertDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, TOTPDevice device) + private static int insertDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + TOTPDevice device) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUserDevicesTable() - + " (app_id, user_id, device_name, secret_key, period, skew, verified, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + + + " (app_id, user_id, device_name, secret_key, period, skew, verified, created_at) VALUES (?, ?, ?, ?, " + + "?, ?, ?, ?)"; return update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -136,13 +141,61 @@ private static int insertDevice_Transaction(Start start, Connection con, AppIden }); } - public static void createDevice_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, TOTPDevice device) + public static void createDevice_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + TOTPDevice device) throws SQLException, StorageQueryException { insertUser_Transaction(start, sqlCon, appIdentifier, device.userId); insertDevice_Transaction(start, sqlCon, appIdentifier, device); } - public static TOTPDevice getDeviceByName_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId, String deviceName) + public static void createDevices_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + List devices) + throws SQLException, StorageQueryException { + + String insert_user_QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUsersTable() + + " (app_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; + + String insert_device_QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUserDevicesTable() + + + " (app_id, user_id, device_name, secret_key, period, skew, verified, created_at) VALUES (?, ?, ?, ?, " + + "?, ?, ?, ?) ON CONFLICT (app_id, user_id, device_name) DO UPDATE SET secret_key = ?, period = ?, skew = ?, created_at = ?, verified = ?"; + + PreparedStatement insertUserStatement = sqlCon.prepareStatement(insert_user_QUERY); + PreparedStatement insertDeviceStatement = sqlCon.prepareStatement(insert_device_QUERY); + + int counter = 0; + for(TOTPDevice device : devices){ + insertUserStatement.setString(1, appIdentifier.getAppId()); + insertUserStatement.setString(2, device.userId); + insertUserStatement.addBatch(); + + insertDeviceStatement.setString(1, appIdentifier.getAppId()); + insertDeviceStatement.setString(2, device.userId); + insertDeviceStatement.setString(3, device.deviceName); + insertDeviceStatement.setString(4, device.secretKey); + insertDeviceStatement.setInt(5, device.period); + insertDeviceStatement.setInt(6, device.skew); + insertDeviceStatement.setBoolean(7, device.verified); + insertDeviceStatement.setLong(8, device.createdAt); + insertDeviceStatement.setString(9, device.secretKey); + insertDeviceStatement.setInt(10, device.period); + insertDeviceStatement.setInt(11, device.skew); + insertDeviceStatement.setLong(12, device.createdAt); + insertDeviceStatement.setBoolean(13, device.verified); + insertDeviceStatement.addBatch(); + counter++; + if(counter % 100 == 0) { + insertUserStatement.executeBatch(); + insertDeviceStatement.executeBatch(); + } + } + + insertUserStatement.executeBatch(); + insertDeviceStatement.executeBatch(); + } + + public static TOTPDevice getDeviceByName_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String userId, String deviceName) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + " WHERE app_id = ? AND user_id = ? AND device_name = ? FOR UPDATE;"; @@ -170,7 +223,8 @@ public static int markDeviceAsVerified(Start start, AppIdentifier appIdentifier, }); } - public static int deleteDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String deviceName) + public static int deleteDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, + String deviceName) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getTotpUserDevicesTable() + " WHERE app_id = ? AND user_id = ? AND device_name = ?;"; @@ -207,7 +261,8 @@ public static boolean removeUser(Start start, TenantIdentifier tenantIdentifier, return removedUsersCount > 0; } - public static int updateDeviceName(Start start, AppIdentifier appIdentifier, String userId, String oldDeviceName, String newDeviceName) + public static int updateDeviceName(Start start, AppIdentifier appIdentifier, String userId, String oldDeviceName, + String newDeviceName) throws StorageQueryException, SQLException { String QUERY = "UPDATE " + Config.getConfig(start).getTotpUserDevicesTable() + " SET device_name = ? WHERE app_id = ? AND user_id = ? AND device_name = ?;"; @@ -238,7 +293,32 @@ public static TOTPDevice[] getDevices(Start start, AppIdentifier appIdentifier, }); } - public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) + public static Map> getDevicesForMultipleUsers(Start start, AppIdentifier appIdentifier, List userIds) + throws StorageQueryException, SQLException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + ");"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for(int i = 0; i < userIds.size(); i++) { + pst.setString(2+i, userIds.get(i)); + } + }, result -> { + Map> devicesByUserIds = new HashMap<>(); + while (result.next()) { + String userId = result.getString("user_id"); + if (!devicesByUserIds.containsKey(userId)){ + devicesByUserIds.put(userId, new ArrayList<>()); + } + devicesByUserIds.get(userId).add(TOTPDeviceRowMapper.getInstance().map(result)); + } + + return devicesByUserIds; + }); + } + + public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId) throws StorageQueryException, SQLException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + " WHERE app_id = ? AND user_id = ? FOR UPDATE;"; @@ -257,10 +337,13 @@ public static TOTPDevice[] getDevices_Transaction(Start start, Connection con, A } - public static int insertUsedCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, TOTPUsedCode code) + public static int insertUsedCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + TOTPUsedCode code) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUsedCodesTable() - + " (app_id, tenant_id, user_id, code, is_valid, expiry_time_ms, created_time_ms) VALUES (?, ?, ?, ?, ?, ?, ?);"; + + + " (app_id, tenant_id, user_id, code, is_valid, expiry_time_ms, created_time_ms) VALUES (?, ?, ?, ?, " + + "?, ?, ?);"; return update(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -278,7 +361,7 @@ public static int insertUsedCode_Transaction(Start start, Connection con, Tenant * order of creation time. */ public static TOTPUsedCode[] getAllUsedCodesDescOrder_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier, String userId) + TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { // Take a lock based on the user id: String QUERY = "SELECT * FROM " + 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 2a37c9dc..afe63b55 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/ThirdPartyQueries.java @@ -19,19 +19,19 @@ import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; -import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; +import io.supertokens.pluginInterface.thirdparty.ThirdPartyImportUser; import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -117,7 +117,9 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden { // 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, primary_or_recipe_user_time_joined)" + + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " + + "primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -220,6 +222,30 @@ public static List lockEmail_Transaction(Start start, Connection con, }); } + public static List lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List emails) + throws StorageQueryException, SQLException { + if(emails == null || emails.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT user_id FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ") FOR UPDATE"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < emails.size(); i++) { + pst.setString(2 + i, emails.get(i)); + } + }, result -> { + List results = new ArrayList<>(); + while (result.next()) { + results.add(result.getString("user_id")); + } + return results; + }); + } + public static List lockThirdPartyInfoAndTenant_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String thirdPartyId, String thirdPartyUserId) @@ -241,6 +267,38 @@ public static List lockThirdPartyInfoAndTenant_Transaction(Start start, }); } + public static List lockThirdPartyInfoAndTenant_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + Map thirdPartyUserIdToThirdPartyId) + throws SQLException, StorageQueryException { + if(thirdPartyUserIdToThirdPartyId == null || thirdPartyUserIdToThirdPartyId.isEmpty()) { + return new ArrayList<>(); + } + + String QUERY = "SELECT user_id " + + " FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND third_party_id IN ("+Utils.generateCommaSeperatedQuestionMarks( + thirdPartyUserIdToThirdPartyId.size())+") AND third_party_user_id IN ("+ + Utils.generateCommaSeperatedQuestionMarks(thirdPartyUserIdToThirdPartyId.size())+") FOR UPDATE"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int counter = 2; + for (String thirdPartyId : thirdPartyUserIdToThirdPartyId.values()){ + pst.setString(counter++, thirdPartyId); + } + for (String thirdPartyUserId : thirdPartyUserIdToThirdPartyId.keySet()) { + pst.setString(counter++, thirdPartyUserId); + } + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); + } + return finalResult; + }); + } + public static List getUsersInfoUsingIdList(Start start, Set ids, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { @@ -274,7 +332,7 @@ public static List getUsersInfoUsingIdList(Start start, Set } public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, - AppIdentifier appIdentifier) + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { if (ids.size() > 0) { String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " @@ -305,7 +363,7 @@ public static List getUsersInfoUsingIdList_Transaction(Start start, public static List listUserIdsByThirdPartyInfo(Start start, AppIdentifier appIdentifier, - String thirdPartyId, String thirdPartyUserId) + String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " @@ -327,7 +385,8 @@ public static List listUserIdsByThirdPartyInfo(Start start, AppIdentifie }); } - public static List listUserIdsByThirdPartyInfo_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + public static List listUserIdsByThirdPartyInfo_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { @@ -350,6 +409,41 @@ public static List listUserIdsByThirdPartyInfo_Transaction(Start start, }); } + public static List listUserIdsByMultipleThirdPartyInfo_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + Map thirdPartyUserIdToThirdPartyId) + throws SQLException, StorageQueryException { + if(thirdPartyUserIdToThirdPartyId.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id IN ( " + Utils.generateCommaSeperatedQuestionMarks( + thirdPartyUserIdToThirdPartyId.size()) + " ) AND tp.third_party_user_id IN ( " + Utils.generateCommaSeperatedQuestionMarks( + thirdPartyUserIdToThirdPartyId.size()) + " )"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int counter = 2; + for (String thirdpartId : thirdPartyUserIdToThirdPartyId.values()){ + pst.setString(counter, thirdpartId); + counter++; + } + for (String thirdparyUserId : thirdPartyUserIdToThirdPartyId.keySet()){ + pst.setString(counter, thirdparyUserId); + counter++; + } + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + public static String getUserIdByThirdPartyInfo(Start start, TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { @@ -388,7 +482,7 @@ public static void updateUserEmail_Transaction(Start start, Connection con, AppI } private static UserInfoPartial getUserInfoUsingUserId_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String userId) + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id @@ -452,6 +546,33 @@ public static List getPrimaryUserIdUsingEmail_Transaction(Start start, C }); } + public static List getPrimaryUserIdsUsingMultipleEmails_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + List emails) + throws StorageQueryException, SQLException { + if(emails.isEmpty()){ + return new ArrayList<>(); + } + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS ep" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.email IN ( " + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + " )"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < emails.size(); i++) { + pst.setString(2+i, emails.get(i)); + } + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException, UnknownUserIdException { @@ -462,11 +583,14 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC throw new UnknownUserIdException(); } - GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, + sqlCon, tenantIdentifier.toAppIdentifier(), userId); { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, " + + "recipe_id, time_joined, primary_or_recipe_user_time_joined)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -479,14 +603,16 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC pst.setLong(8, userInfo.timeJoined); }); - GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), accountLinkingInfo.primaryUserId); + GeneralQueries.updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), + accountLinkingInfo.primaryUserId); } { // thirdparty_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUserToTenantTable() + "(app_id, tenant_id, user_id, third_party_id, third_party_user_id)" + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT ON CONSTRAINT " - + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), getConfig(start).getThirdPartyUserToTenantTable(), null, "pkey") + + Utils.getConstraintName(Config.getConfig(start).getTableSchema(), + getConfig(start).getThirdPartyUserToTenantTable(), null, "pkey") + " DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -519,6 +645,80 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from thirdparty_user_to_tenant because of foreign key constraint } + public static void importUser_Transaction(Start start, Connection sqlConnection, Collection users) + throws SQLException { + + String app_id_userid_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; + + String all_auth_recipe_users_QUERY = "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(?, ?, ?, ?, ?, ?, ?)"; + + String thirdparty_users_QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUsersTable() + + "(app_id, third_party_id, third_party_user_id, user_id, email, time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?)"; + + String thirdparty_user_to_tenant_QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUserToTenantTable() + + "(app_id, tenant_id, user_id, third_party_id, third_party_user_id)" + + " VALUES(?, ?, ?, ?, ?)"; + + PreparedStatement appIdToUserIdStatement = sqlConnection.prepareStatement(app_id_userid_QUERY); + PreparedStatement allAuthRecipeUsersStatement = sqlConnection.prepareStatement(all_auth_recipe_users_QUERY); + PreparedStatement thirdPartyUsersStatement = sqlConnection.prepareStatement(thirdparty_users_QUERY); + PreparedStatement thirdPartyUsersToTenantStatement = sqlConnection.prepareStatement( + thirdparty_user_to_tenant_QUERY); + + int counter = 0; + for (ThirdPartyImportUser user : users) { + TenantIdentifier tenantIdentifier = user.tenantIdentifier; + appIdToUserIdStatement.setString(1, tenantIdentifier.getAppId()); + appIdToUserIdStatement.setString(2, user.userId); + appIdToUserIdStatement.setString(3, user.userId); + appIdToUserIdStatement.setString(4, THIRD_PARTY.toString()); + appIdToUserIdStatement.addBatch(); + + allAuthRecipeUsersStatement.setString(1, tenantIdentifier.getAppId()); + allAuthRecipeUsersStatement.setString(2, tenantIdentifier.getTenantId()); + allAuthRecipeUsersStatement.setString(3, user.userId); + allAuthRecipeUsersStatement.setString(4, user.userId); + allAuthRecipeUsersStatement.setString(5, THIRD_PARTY.toString()); + allAuthRecipeUsersStatement.setLong(6, user.timeJoinedMSSinceEpoch); + allAuthRecipeUsersStatement.setLong(7, user.timeJoinedMSSinceEpoch); + allAuthRecipeUsersStatement.addBatch(); + + thirdPartyUsersStatement.setString(1, tenantIdentifier.getAppId()); + thirdPartyUsersStatement.setString(2, user.thirdpartyId); + thirdPartyUsersStatement.setString(3, user.thirdpartyUserId); + thirdPartyUsersStatement.setString(4, user.userId); + thirdPartyUsersStatement.setString(5, user.email); + thirdPartyUsersStatement.setLong(6, user.timeJoinedMSSinceEpoch); + thirdPartyUsersStatement.addBatch(); + + thirdPartyUsersToTenantStatement.setString(1, tenantIdentifier.getAppId()); + thirdPartyUsersToTenantStatement.setString(2, tenantIdentifier.getTenantId()); + thirdPartyUsersToTenantStatement.setString(3, user.userId); + thirdPartyUsersToTenantStatement.setString(4, user.thirdpartyId); + thirdPartyUsersToTenantStatement.setString(5, user.thirdpartyUserId); + thirdPartyUsersToTenantStatement.addBatch(); + + counter++; + if(counter % 100 == 0) { + appIdToUserIdStatement.executeBatch(); + allAuthRecipeUsersStatement.executeBatch(); + thirdPartyUsersStatement.executeBatch(); + thirdPartyUsersToTenantStatement.executeBatch(); + } + } + + appIdToUserIdStatement.executeBatch(); + allAuthRecipeUsersStatement.executeBatch(); + thirdPartyUsersStatement.executeBatch(); + thirdPartyUsersToTenantStatement.executeBatch(); + } + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java index a2388765..48ea4dd1 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserIdMappingQueries.java @@ -20,17 +20,20 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; +import io.supertokens.storage.postgresql.ConnectionPool; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; import javax.annotation.Nullable; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -55,7 +58,8 @@ public static String getQueryToCreateUserIdMappingTable(Start start) { + " PRIMARY KEY(app_id, supertokens_user_id, external_user_id)," + "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "fkey") + " FOREIGN KEY (app_id, supertokens_user_id)" - + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id)" + " ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + "(app_id, user_id)" + + " ON DELETE CASCADE" + ");"; // @formatter:on } @@ -65,7 +69,8 @@ public static String getQueryToCreateSupertokensUserIdIndexForUserIdMappingTable + getConfig(start).getUserIdMappingTable() + "(app_id, supertokens_user_id);"; } - public static void createUserIdMapping(Start start, AppIdentifier appIdentifier, String superTokensUserId, String externalUserId, + public static void createUserIdMapping(Start start, AppIdentifier appIdentifier, String superTokensUserId, + String externalUserId, String externalUserIdInfo) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserIdMappingTable() + " (app_id, supertokens_user_id, external_user_id, external_user_id_info)" + " VALUES(?, ?, ?, ?)"; @@ -78,7 +83,32 @@ public static void createUserIdMapping(Start start, AppIdentifier appIdentifier, }); } - public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, String userId) + public static void createBulkUserIdMapping(Start start, AppIdentifier appIdentifier, + Map superTokensUserIdToExternalUserId) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getUserIdMappingTable() + + " (app_id, supertokens_user_id, external_user_id)" + " VALUES(?, ?, ?)"; + + Connection sqlConnection = ConnectionPool.getConnection(start); + PreparedStatement insertStatement = sqlConnection.prepareStatement(QUERY); + + int counter = 0; + for(String superTokensUserId : superTokensUserIdToExternalUserId.keySet()) { + insertStatement.setString(1, appIdentifier.getAppId()); + insertStatement.setString(2, superTokensUserId); + insertStatement.setString(3, superTokensUserIdToExternalUserId.get(superTokensUserId)); + insertStatement.addBatch(); + + counter++; + if(counter % 100 == 0) { + insertStatement.executeBatch(); + } + } + insertStatement.executeBatch(); + } + + public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND supertokens_user_id = ?"; @@ -93,7 +123,8 @@ public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, }); } - public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, AppIdentifier appIdentifier, String userId) + public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND external_user_id = ?"; @@ -110,7 +141,9 @@ public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, AppI } public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(Start start, - AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND (supertokens_user_id = ? OR external_user_id = ?)"; @@ -201,7 +234,8 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St }); } - public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, String userId) + public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND supertokens_user_id = ?"; @@ -217,7 +251,8 @@ public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, AppI public static boolean deleteUserIdMappingWithExternalUserId(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND external_user_id = ?"; + String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND external_user_id = ?"; // store the number of rows updated int rowUpdatedCount = update(start, QUERY, pst -> { @@ -229,8 +264,10 @@ public static boolean deleteUserIdMappingWithExternalUserId(Start start, AppIden } public static boolean updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(Start start, - AppIdentifier appIdentifier, String userId, - @Nullable String externalUserIdInfo) throws SQLException, StorageQueryException { + AppIdentifier appIdentifier, + String userId, + @Nullable String externalUserIdInfo) + throws SQLException, StorageQueryException { String QUERY = "UPDATE " + Config.getConfig(start).getUserIdMappingTable() + " SET external_user_id_info = ? WHERE app_id = ? AND supertokens_user_id = ?"; @@ -245,7 +282,8 @@ public static boolean updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(Star public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start start, AppIdentifier appIdentifier, String userId, - @Nullable String externalUserIdInfo) throws SQLException, StorageQueryException { + @Nullable String externalUserIdInfo) + throws SQLException, StorageQueryException { String QUERY = "UPDATE " + Config.getConfig(start).getUserIdMappingTable() + " SET external_user_id_info = ? WHERE app_id = ? AND external_user_id = ?"; @@ -258,7 +296,9 @@ public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start s return rowUpdated > 0; } - public static UserIdMapping getuseraIdMappingWithSuperTokensUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + public static UserIdMapping getuseraIdMappingWithSuperTokensUserId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND supertokens_user_id = ?"; @@ -273,7 +313,9 @@ public static UserIdMapping getuseraIdMappingWithSuperTokensUserId_Transaction(S }); } - public static UserIdMapping getUserIdMappingWithExternalUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + public static UserIdMapping getUserIdMappingWithExternalUserId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND external_user_id = ?"; @@ -289,8 +331,54 @@ public static UserIdMapping getUserIdMappingWithExternalUserId_Transaction(Start }); } - public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(Start start, Connection sqlCon, - AppIdentifier appIdentifier, String userId) + public static List getMultipleUserIdMappingWithExternalUserId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND external_user_id IN ( "+ Utils.generateCommaSeperatedQuestionMarks( + userId.size()) + " )"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for(int i = 0; i < userId.size(); i++) { + pst.setString(2 + i, userId.get(i)); + } + }, result -> { + List results = new ArrayList<>(); + while (result.next()) { + results.add(UserIdMappingRowMapper.getInstance().mapOrThrow(result)); + } + return results; + }); + } + + public static List getMultipleUserIdMappingWithSupertokensUserId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND supertokens_user_id IN ( "+ Utils.generateCommaSeperatedQuestionMarks( + userId.size()) + " )"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for(int i = 0; i < userId.size(); i++) { + pst.setString(2 + i, userId.get(i)); + } + }, result -> { + List results = new ArrayList<>(); + while (result.next()) { + results.add(UserIdMappingRowMapper.getInstance().mapOrThrow(result)); + } + return results; + }); + } + + public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(Start start, + Connection sqlCon, + AppIdentifier appIdentifier, + String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND (supertokens_user_id = ? OR external_user_id = ?)"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java index 1d2b6231..d4fde3d7 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java @@ -19,13 +19,18 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -93,6 +98,31 @@ public static int setUserMetadata_Transaction(Start start, Connection con, AppId }); } + public static void setMultipleUsersMetadatas_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + Map metadatasByUserId) + throws SQLException, StorageQueryException { + + String QUERY = "INSERT INTO " + getConfig(start).getUserMetadataTable() + + "(app_id, user_id, user_metadata) VALUES(?, ?, ?) " + + "ON CONFLICT(app_id, user_id) DO UPDATE SET user_metadata=excluded.user_metadata;"; + PreparedStatement insertStatement = con.prepareStatement(QUERY); + + int counter = 0; + for(Map.Entry metadataByUserId : metadatasByUserId.entrySet()){ + insertStatement.setString(1, appIdentifier.getAppId()); + insertStatement.setString(2, metadataByUserId.getKey()); + insertStatement.setString(3, metadataByUserId.getValue().toString()); + insertStatement.addBatch(); + + counter++; + if(counter % 100 == 0) { + insertStatement.executeBatch(); + } + } + + insertStatement.executeBatch(); + } + public static JsonObject getUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { @@ -110,6 +140,28 @@ public static JsonObject getUserMetadata_Transaction(Start start, Connection con }); } + public static Map getMultipleUsersMetadatas_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + List userIds) + throws SQLException, StorageQueryException { + String QUERY = "SELECT user_id, user_metadata FROM " + getConfig(start).getUserMetadataTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i< userIds.size(); i++){ + pst.setString(2+i, userIds.get(i)); + } + }, result -> { + Map userMetadataByUserId = new HashMap<>(); + JsonParser jp = new JsonParser(); + if (result.next()) { + userMetadataByUserId.put(result.getString("user_id"), + jp.parse(result.getString("user_metadata")).getAsJsonObject()); + } + return userMetadataByUserId; + }); + } + public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable() @@ -125,4 +177,11 @@ public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifie return null; }); } + + public static Map getMultipleUserMetadatas(Start start, AppIdentifier appIdentifier, List userIds) + throws StorageQueryException, StorageTransactionLogicException { + return start.startTransaction(con -> { + return getMultipleUsersMetadatas_Transaction(start, (Connection) con.getConnection(), appIdentifier, userIds); + }); + } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java index 10fcb1a7..807c8bf3 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserRolesQueries.java @@ -17,18 +17,20 @@ package io.supertokens.storage.postgresql.queries; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; @@ -142,7 +144,7 @@ public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start star } public static boolean deleteRole(Start start, AppIdentifier appIdentifier, - String role) + String role) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ? AND role = ? ;"; @@ -202,6 +204,33 @@ public static int addRoleToUser(Start start, TenantIdentifier tenantIdentifier, }); } + public static void addRolesToUsers_Transaction(Start start, Connection connection, Map>> rolesToUserByTenants) //tenant -> user -> role + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + getConfig(start).getUserRolesTable() + + "(app_id, tenant_id, user_id, role) VALUES(?, ?, ?, ?);"; + PreparedStatement insertStatement = connection.prepareStatement(QUERY); + + int counter = 0; + for(Map.Entry>> tenantsEntry : rolesToUserByTenants.entrySet()) { + for(Map.Entry> rolesToUser : tenantsEntry.getValue().entrySet()) { + for(String roleForUser : rolesToUser.getValue()){ + insertStatement.setString(1, tenantsEntry.getKey().getAppId()); + insertStatement.setString(2, tenantsEntry.getKey().getTenantId()); + insertStatement.setString(3, rolesToUser.getKey()); + insertStatement.setString(4, roleForUser); + insertStatement.addBatch(); + counter++; + + if(counter % 100 == 0) { + insertStatement.executeBatch(); + } + } + } + } + + insertStatement.executeBatch(); + } + public static String[] getRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "SELECT role FROM " + getConfig(start).getUserRolesTable() @@ -237,6 +266,29 @@ public static String[] getRolesForUser(Start start, AppIdentifier appIdentifier, }); } + public static Map> getRolesForUsers(Start start, AppIdentifier appIdentifier, List userIds) + throws SQLException, StorageQueryException { + String QUERY = "SELECT user_id, role FROM " + getConfig(start).getUserRolesTable() + + " WHERE app_id = ? AND user_id IN ("+Utils.generateCommaSeperatedQuestionMarks(userIds.size())+") ;"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for(int i = 0; i < userIds.size(); i++) { + pst.setString(2+i, userIds.get(i)); + } + }, result -> { + Map> rolesByUserId = new HashMap<>(); + while (result.next()) { + String userId = result.getString("user_id"); + if(!rolesByUserId.containsKey(userId)) { + rolesByUserId.put(userId, new ArrayList<>()); + } + rolesByUserId.get(userId).add(result.getString("role")); + } + return rolesByUserId; + }); + } + public static boolean deleteRoleForUser_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String userId, String role) throws SQLException, StorageQueryException { @@ -264,6 +316,25 @@ public static boolean doesRoleExist_transaction(Start start, Connection con, App }, ResultSet::next); } + public static List doesMultipleRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier, + List roles) + throws SQLException, StorageQueryException { + String QUERY = "SELECT role FROM " + getConfig(start).getRolesTable() + + " WHERE app_id = ? AND role IN (" +Utils.generateCommaSeperatedQuestionMarks(roles.size())+ ") FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < roles.size(); i++) { + pst.setString(2+i, roles.get(i)); + } + }, result -> { + List rolesFound = new ArrayList<>(); + while(result.next()){ + rolesFound.add(result.getString("role")); + } + return rolesFound; + }); + } + public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id FROM " + getConfig(start).getUserRolesTable() diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java index b5abf91d..ccbab4f6 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/MfaSqlHelper.java @@ -33,11 +33,13 @@ public static HashMap selectAllFirstFactors(Start st throws SQLException, StorageQueryException { String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id FROM " + getConfig(start).getTenantFirstFactorsTable() + ";"; - return execute(start, QUERY, pst -> {}, result -> { + return execute(start, QUERY, pst -> { + }, result -> { HashMap> firstFactors = new HashMap<>(); while (result.next()) { - TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), + result.getString("app_id"), result.getString("tenant_id")); if (!firstFactors.containsKey(tenantIdentifier)) { firstFactors.put(tenantIdentifier, new ArrayList<>()); } @@ -58,7 +60,8 @@ public static HashMap selectAllRequiredSecondaryFact throws SQLException, StorageQueryException { String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id FROM " + getConfig(start).getTenantRequiredSecondaryFactorsTable() + ";"; - return execute(start, QUERY, pst -> {}, result -> { + return execute(start, QUERY, pst -> { + }, result -> { HashMap> defaultRequiredFactors = new HashMap<>(); while (result.next()) { @@ -80,16 +83,18 @@ public static HashMap selectAllRequiredSecondaryFact }); } - public static void createFirstFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] firstFactors) + public static void createFirstFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, + String[] firstFactors) throws SQLException, StorageQueryException { if (firstFactors == null || firstFactors.length == 0) { return; } - String QUERY = "INSERT INTO " + getConfig(start).getTenantFirstFactorsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; + String QUERY = "INSERT INTO " + getConfig(start).getTenantFirstFactorsTable() + + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; for (String factorId : new HashSet<>(Arrays.asList(firstFactors))) { update(sqlCon, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getConnectionUriDomain()); + pst.setString(1, tenantIdentifier.getConnectionUriDomain()); pst.setString(2, tenantIdentifier.getAppId()); pst.setString(3, tenantIdentifier.getTenantId()); pst.setString(4, factorId); @@ -97,13 +102,15 @@ public static void createFirstFactors(Start start, Connection sqlCon, TenantIden } } - public static void createRequiredSecondaryFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] requiredSecondaryFactors) + public static void createRequiredSecondaryFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, + String[] requiredSecondaryFactors) throws SQLException, StorageQueryException { if (requiredSecondaryFactors == null || requiredSecondaryFactors.length == 0) { return; } - String QUERY = "INSERT INTO " + getConfig(start).getTenantRequiredSecondaryFactorsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; + String QUERY = "INSERT INTO " + getConfig(start).getTenantRequiredSecondaryFactorsTable() + + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; for (String factorId : requiredSecondaryFactors) { update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getConnectionUriDomain()); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java index 1a2e00b2..79b4fb18 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/TenantConfigSQLHelper.java @@ -16,15 +16,11 @@ package io.supertokens.storage.postgresql.queries.multitenancy; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.queries.utils.JsonUtils; -import io.supertokens.storage.postgresql.utils.Utils; import java.sql.Connection; import java.sql.ResultSet; @@ -43,25 +39,32 @@ public static class TenantConfigRowMapper implements RowMapper> providerMap, HashMap firstFactorsMap, HashMap requiredSecondaryFactorsMap) + public static TenantConfig[] selectAll(Start start, + HashMap> providerMap, + HashMap firstFactorsMap, + HashMap requiredSecondaryFactorsMap) throws SQLException, StorageQueryException { String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, core_config," - + " email_password_enabled, passwordless_enabled, third_party_enabled FROM " + + " email_password_enabled, passwordless_enabled, third_party_enabled, " + + " is_first_factors_null FROM " + getConfig(start).getTenantConfigsTable() + ";"; - TenantConfig[] tenantConfigs = execute(start, QUERY, pst -> {}, result -> { + TenantConfig[] tenantConfigs = execute(start, QUERY, pst -> { + }, result -> { List temp = new ArrayList<>(); while (result.next()) { - TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); - ThirdPartyConfig.Provider[] providers = null; + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), + result.getString("app_id"), result.getString("tenant_id")); + ThirdPartyConfig.Provider[] providers; if (providerMap.containsKey(tenantIdentifier)) { providers = providerMap.get(tenantIdentifier).values().toArray(new ThirdPartyConfig.Provider[0]); + } else { + providers = new ThirdPartyConfig.Provider[0]; } - String[] firstFactors = firstFactorsMap.containsKey(tenantIdentifier) ? firstFactorsMap.get(tenantIdentifier) : new String[0]; + String[] firstFactors = + firstFactorsMap.containsKey(tenantIdentifier) ? firstFactorsMap.get(tenantIdentifier) : + new String[0]; - String[] requiredSecondaryFactors = requiredSecondaryFactorsMap.containsKey(tenantIdentifier) ? requiredSecondaryFactorsMap.get(tenantIdentifier) : new String[0]; + String[] requiredSecondaryFactors = requiredSecondaryFactorsMap.containsKey(tenantIdentifier) ? + requiredSecondaryFactorsMap.get(tenantIdentifier) : new String[0]; - temp.add(TenantConfigSQLHelper.TenantConfigRowMapper.getInstance(providers, firstFactors, requiredSecondaryFactors).mapOrThrow(result)); + temp.add(TenantConfigSQLHelper.TenantConfigRowMapper.getInstance(providers, firstFactors, + requiredSecondaryFactors).mapOrThrow(result)); } TenantConfig[] finalResult = new TenantConfig[temp.size()]; for (int i = 0; i < temp.size(); i++) { @@ -104,8 +119,9 @@ public static void create(Start start, Connection sqlCon, TenantConfig tenantCon throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getTenantConfigsTable() + "(connection_uri_domain, app_id, tenant_id, core_config," - + " email_password_enabled, passwordless_enabled, third_party_enabled)" - + " VALUES(?, ?, ?, ?, ?, ?, ?)"; + + " email_password_enabled, passwordless_enabled, third_party_enabled," + + " is_first_factors_null)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); @@ -115,6 +131,7 @@ public static void create(Start start, Connection sqlCon, TenantConfig tenantCon pst.setBoolean(5, tenantConfig.emailPasswordConfig.enabled); pst.setBoolean(6, tenantConfig.passwordlessConfig.enabled); pst.setBoolean(7, tenantConfig.thirdPartyConfig.enabled); + pst.setBoolean(8, tenantConfig.firstFactors == null); }); } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java index ced73d6e..100194ea 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderClientSQLHelper.java @@ -35,7 +35,8 @@ public class ThirdPartyProviderClientSQLHelper { public static class TenantThirdPartyProviderClientRowMapper implements RowMapper { - public static final ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper INSTANCE = new ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper(); + public static final ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper INSTANCE = + new ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper(); private TenantThirdPartyProviderClientRowMapper() { } @@ -77,37 +78,48 @@ public ThirdPartyConfig.ProviderClient map(ResultSet result) throws StorageQuery } } - public static HashMap>> selectAll(Start start) + public static HashMap>> selectAll( + Start start) throws SQLException, StorageQueryException { HashMap>> providerClientsMap = new HashMap<>(); - String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, scope, force_pkce, additional_config FROM " - + getConfig(start).getTenantThirdPartyProviderClientsTable() + ";"; + String QUERY = + "SELECT connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, " + + "client_secret, scope, force_pkce, additional_config FROM " + + getConfig(start).getTenantThirdPartyProviderClientsTable() + ";"; - execute(start, QUERY, pst -> {}, result -> { + execute(start, QUERY, pst -> { + }, result -> { while (result.next()) { - TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); - ThirdPartyConfig.ProviderClient providerClient = ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper.getInstance().mapOrThrow(result); + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), + result.getString("app_id"), result.getString("tenant_id")); + ThirdPartyConfig.ProviderClient providerClient = + ThirdPartyProviderClientSQLHelper.TenantThirdPartyProviderClientRowMapper.getInstance() + .mapOrThrow(result); if (!providerClientsMap.containsKey(tenantIdentifier)) { providerClientsMap.put(tenantIdentifier, new HashMap<>()); } - if(!providerClientsMap.get(tenantIdentifier).containsKey(result.getString("third_party_id"))) { + if (!providerClientsMap.get(tenantIdentifier).containsKey(result.getString("third_party_id"))) { providerClientsMap.get(tenantIdentifier).put(result.getString("third_party_id"), new HashMap<>()); } - providerClientsMap.get(tenantIdentifier).get(result.getString("third_party_id")).put(providerClient.clientType, providerClient); + providerClientsMap.get(tenantIdentifier).get(result.getString("third_party_id")) + .put(providerClient.clientType, providerClient); } return null; }); return providerClientsMap; } - public static void create(Start start, Connection sqlCon, TenantConfig tenantConfig, ThirdPartyConfig.Provider provider, ThirdPartyConfig.ProviderClient providerClient) + public static void create(Start start, Connection sqlCon, TenantConfig tenantConfig, + ThirdPartyConfig.Provider provider, ThirdPartyConfig.ProviderClient providerClient) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getTenantThirdPartyProviderClientsTable() - + "(connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, scope, force_pkce, additional_config)" + + + "(connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, " + + "scope, force_pkce, additional_config)" + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; Array scopeArray; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java index 3f2b6645..321f80d5 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/multitenancy/ThirdPartyProviderSQLHelper.java @@ -40,7 +40,8 @@ private TenantThirdPartyProviderRowMapper(ThirdPartyConfig.ProviderClient[] clie this.clients = clients; } - public static ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper getInstance(ThirdPartyConfig.ProviderClient[] clients) { + public static ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper getInstance( + ThirdPartyConfig.ProviderClient[] clients) { return new ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper(clients); } @@ -82,21 +83,36 @@ public ThirdPartyConfig.Provider map(ResultSet result) throws StorageQueryExcept } } - public static HashMap> selectAll(Start start, HashMap>> providerClientsMap) + public static HashMap> selectAll(Start start, + HashMap>> providerClientsMap) throws SQLException, StorageQueryException { HashMap> providerMap = new HashMap<>(); - String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, third_party_id, name, authorization_endpoint, authorization_endpoint_query_params, token_endpoint, token_endpoint_body_params, user_info_endpoint, user_info_endpoint_query_params, user_info_endpoint_headers, jwks_uri, oidc_discovery_endpoint, require_email, user_info_map_from_id_token_payload_user_id, user_info_map_from_id_token_payload_email, user_info_map_from_id_token_payload_email_verified, user_info_map_from_user_info_endpoint_user_id, user_info_map_from_user_info_endpoint_email, user_info_map_from_user_info_endpoint_email_verified FROM " - + getConfig(start).getTenantThirdPartyProvidersTable() + ";"; + String QUERY = + "SELECT connection_uri_domain, app_id, tenant_id, third_party_id, name, authorization_endpoint, " + + "authorization_endpoint_query_params, token_endpoint, token_endpoint_body_params, " + + "user_info_endpoint, user_info_endpoint_query_params, user_info_endpoint_headers, jwks_uri, " + + "oidc_discovery_endpoint, require_email, user_info_map_from_id_token_payload_user_id, " + + "user_info_map_from_id_token_payload_email, " + + "user_info_map_from_id_token_payload_email_verified, " + + "user_info_map_from_user_info_endpoint_user_id, user_info_map_from_user_info_endpoint_email, " + + "user_info_map_from_user_info_endpoint_email_verified FROM " + + getConfig(start).getTenantThirdPartyProvidersTable() + ";"; - execute(start, QUERY, pst -> {}, result -> { + execute(start, QUERY, pst -> { + }, result -> { while (result.next()) { - TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), + result.getString("app_id"), result.getString("tenant_id")); ThirdPartyConfig.ProviderClient[] clients = null; - if (providerClientsMap.containsKey(tenantIdentifier) && providerClientsMap.get(tenantIdentifier).containsKey(result.getString("third_party_id"))) { - clients = providerClientsMap.get(tenantIdentifier).get(result.getString("third_party_id")).values().toArray(new ThirdPartyConfig.ProviderClient[0]); + if (providerClientsMap.containsKey(tenantIdentifier) && + providerClientsMap.get(tenantIdentifier).containsKey(result.getString("third_party_id"))) { + clients = providerClientsMap.get(tenantIdentifier).get(result.getString("third_party_id")).values() + .toArray(new ThirdPartyConfig.ProviderClient[0]); } - ThirdPartyConfig.Provider provider = ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper.getInstance(clients).mapOrThrow(result); + ThirdPartyConfig.Provider provider = + ThirdPartyProviderSQLHelper.TenantThirdPartyProviderRowMapper.getInstance( + clients).mapOrThrow(result); if (!providerMap.containsKey(tenantIdentifier)) { providerMap.put(tenantIdentifier, new HashMap<>()); @@ -108,10 +124,19 @@ public static HashMap { pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java index 6891f739..755b6720 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/ConfigTest.java @@ -662,9 +662,12 @@ public void testAllConfigsHaveAnAnnotation() throws Exception { continue; } - if (!(field.isAnnotationPresent(UserPoolProperty.class) || field.isAnnotationPresent(ConnectionPoolProperty.class) || field.isAnnotationPresent( + if (!(field.isAnnotationPresent(UserPoolProperty.class) || + field.isAnnotationPresent(ConnectionPoolProperty.class) || field.isAnnotationPresent( NotConflictingWithinUserPool.class))) { - fail(field.getName() + " does not have UserPoolProperty, ConnectionPoolProperty or NotConflictingWithinUserPool annotation"); + fail(field.getName() + + " does not have UserPoolProperty, ConnectionPoolProperty or NotConflictingWithinUserPool " + + "annotation"); } } } diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java index 470e6ce0..3a7f5935 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DbConnectionPoolTest.java @@ -154,14 +154,15 @@ public void testDownTimeWhenChangingConnectionPoolSize() throws Exception { try { TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); Storage t1Storage = (StorageLayer.getStorage(t1, process.getProcess())); - ThirdParty.signInUp(t1, t1Storage, process.getProcess(), "google", "googleid"+ finalI, "user" + + ThirdParty.signInUp(t1, t1Storage, process.getProcess(), "google", "googleid" + finalI, "user" + finalI + "@example.com"); if (firstErrorTime.get() != -1 && successAfterErrorTime.get() == -1) { successAfterErrorTime.set(System.currentTimeMillis()); } } catch (StorageQueryException e) { - if (e.getMessage().contains("Connection is closed") || e.getMessage().contains("has been closed")) { + if (e.getMessage().contains("Connection is closed") || + e.getMessage().contains("has been closed")) { if (firstErrorTime.get() == -1) { firstErrorTime.set(System.currentTimeMillis()); } @@ -286,6 +287,16 @@ public void testMinimumIdleConnectionForTenants() throws Exception { Thread.sleep(1000); // let the new tenant be ready + for (int retry = 0; retry < 5; retry++) { + try { + assertEquals(10, start.getDbActivityCount("st1")); + break; + } catch (AssertionError e) { + Thread.sleep(1000); + continue; + } + } + assertEquals(10, start.getDbActivityCount("st1")); // change connection pool size @@ -355,7 +366,7 @@ public void testIdleConnectionTimeout() throws Exception { try { TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); Storage t1Storage = (StorageLayer.getStorage(t1, process.getProcess())); - ThirdParty.signInUp(t1, t1Storage, process.getProcess(), "google", "googleid"+ finalI, "user" + + ThirdParty.signInUp(t1, t1Storage, process.getProcess(), "google", "googleid" + finalI, "user" + finalI + "@example.com"); } catch (StorageQueryException e) { diff --git a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java index 22fe29bb..f86faadf 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/DeadlockTest.java @@ -263,9 +263,11 @@ public void testCodeCreationRapidlyWithDifferentEmails() throws Exception { System.out.println("Durations: " + durations.toString()); assertNull(process - .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); assertNotNull(process - .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); assert (pass.get()); @@ -627,7 +629,8 @@ public void testLinkAccountsInParallel() throws Exception { for (int i = 0; i < 3000; i++) { es.execute(() -> { try { - AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), + user1.getSupertokensUserId()); AuthRecipe.unlinkAccounts(process.getProcess(), user2.getSupertokensUserId()); } catch (Exception e) { if (e.getMessage().toLowerCase().contains("the transaction might succeed if retried")) { @@ -642,9 +645,11 @@ public void testLinkAccountsInParallel() throws Exception { assert (pass.get()); assertNull(process - .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); assertNotNull(process - .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -685,9 +690,11 @@ public void testCreatePrimaryInParallel() throws Exception { assert (pass.get()); assertNull(process - .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_NOT_RESOLVED)); assertNotNull(process - .checkOrWaitForEventInPlugin(io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); + .checkOrWaitForEventInPlugin( + io.supertokens.storage.postgresql.ProcessState.PROCESS_STATE.DEADLOCK_FOUND)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/LogLevelTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LogLevelTest.java index 996ed4d2..e675c765 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LogLevelTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LogLevelTest.java @@ -52,7 +52,7 @@ public void beforeEach() { public void testLogLevelNoneOutput() throws Exception { { Utils.setValueInConfig("log_level", "NONE"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -96,7 +96,7 @@ public void testLogLevelNoneOutput() throws Exception { public void testLogLevelErrorOutput() throws Exception { { Utils.setValueInConfig("log_level", "ERROR"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -150,7 +150,7 @@ public void testLogLevelErrorOutput() throws Exception { public void testLogLevelWarnOutput() throws Exception { { Utils.setValueInConfig("log_level", "WARN"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -204,7 +204,7 @@ public void testLogLevelWarnOutput() throws Exception { public void testLogLevelInfoOutput() throws Exception { { Utils.setValueInConfig("log_level", "INFO"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -258,7 +258,7 @@ public void testLogLevelInfoOutput() throws Exception { public void testLogLevelDebugOutput() throws Exception { { Utils.setValueInConfig("log_level", "DEBUG"); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java index 591a4ac0..52602c2c 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/LoggingTest.java @@ -44,7 +44,6 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Iterator; -import java.util.List; import java.util.Scanner; import static org.junit.Assert.*; @@ -65,7 +64,7 @@ public void beforeEach() { @Test public void defaultLogging() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; StorageLayer.close(); TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -109,7 +108,7 @@ public void defaultLogging() throws Exception { @Test public void customLogging() throws Exception { try { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("info_log_path", "\"tempLogging/info.log\""); Utils.setValueInConfig("error_log_path", "\"tempLogging/error.log\""); @@ -160,7 +159,7 @@ public void customLogging() throws Exception { @Test public void confirmLoggerClosed() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; StorageLayer.close(); TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -188,7 +187,7 @@ public void confirmLoggerClosed() throws Exception { @Test public void testStandardOutLoggingWithNullStr() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; ByteArrayOutputStream stdOutput = new ByteArrayOutputStream(); ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); @@ -224,7 +223,7 @@ public void testStandardOutLoggingWithNullStr() throws Exception { @Test public void testStandardOutLoggingWithNull() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; ByteArrayOutputStream stdOutput = new ByteArrayOutputStream(); ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); @@ -261,7 +260,7 @@ public void testStandardOutLoggingWithNull() throws Exception { @Test public void confirmHikariLoggerClosedOnlyWhenProcessEnds() throws Exception { StorageLayer.close(); - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); @@ -315,7 +314,7 @@ public void confirmHikariLoggerClosedOnlyWhenProcessEnds() throws Exception { @Test public void testDBPasswordMaskingOnDBConnectionFailUsingConnectionUri() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; String dbUser = "db_user"; String dbPassword = "db_password"; @@ -347,7 +346,7 @@ public void testDBPasswordMaskingOnDBConnectionFailUsingConnectionUri() throws E @Test public void testDBPasswordMaskingOnDBConnectionFailUsingCredentials() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; String dbUser = "db_user"; String dbPassword = "db_password"; @@ -381,7 +380,7 @@ public void testDBPasswordMaskingOnDBConnectionFailUsingCredentials() throws Exc @Test public void testDBPasswordMasking() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; ByteArrayOutputStream stdOutput = new ByteArrayOutputStream(); ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); @@ -416,7 +415,7 @@ public void testDBPasswordMasking() throws Exception { @Test public void testDBPasswordIsNotLoggedWhenProcessStartsEnds() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("error_log_path", "null"); Utils.setValueInConfig("info_log_path", "null"); @@ -479,7 +478,7 @@ public void testDBPasswordIsNotLoggedWhenProcessStartsEnds() throws Exception { @Test public void testDBPasswordIsNotLoggedWhenTenantIsCreated() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("error_log_path", "null"); Utils.setValueInConfig("info_log_path", "null"); @@ -505,8 +504,8 @@ public void testDBPasswordIsNotLoggedWhenTenantIsCreated() throws Exception { Main main = process.getProcess(); FeatureFlagTestContent.getInstance(main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY }); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); JsonObject config = new JsonObject(); TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); @@ -548,8 +547,8 @@ public void testDBPasswordIsNotLoggedWhenTenantIsCreated() throws Exception { Main main = process.getProcess(); FeatureFlagTestContent.getInstance(main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY }); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); JsonObject config = new JsonObject(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java index 37346a11..ea29a8f9 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/OneMillionUsersTest.java @@ -27,8 +27,11 @@ import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.ParsedFirebaseSCryptResponse; import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.passwordless.Passwordless; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; @@ -39,6 +42,7 @@ import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; import io.supertokens.session.Session; import io.supertokens.session.info.SessionInformationHolder; +import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.test.httpRequest.HttpRequestForTesting; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.thirdparty.ThirdParty; @@ -107,7 +111,8 @@ private void createEmailPasswordUsers(Main main) throws Exception { String userId = io.supertokens.utils.Utils.getUUID(); long timeJoined = System.currentTimeMillis(); - storage.signUp(TenantIdentifier.BASE_TENANT, userId, "eptest" + finalI + "@example.com", combinedPasswordHash, + storage.signUp(TenantIdentifier.BASE_TENANT, userId, "eptest" + finalI + "@example.com", + combinedPasswordHash, timeJoined); synchronized (lock) { allUserIds.add(userId); @@ -116,7 +121,7 @@ private void createEmailPasswordUsers(Main main) throws Exception { throw new RuntimeException(e); } if (finalI % 10000 == 9999) { - System.out.println("Created " + ((finalI +1)) + " users"); + System.out.println("Created " + ((finalI + 1)) + " users"); } }); } @@ -137,7 +142,8 @@ private void createPasswordlessUsersWithEmail(Main main) throws Exception { String userId = io.supertokens.utils.Utils.getUUID(); long timeJoined = System.currentTimeMillis(); try { - storage.createUser(TenantIdentifier.BASE_TENANT, userId, "pltest" + finalI + "@example.com", null, timeJoined); + storage.createUser(TenantIdentifier.BASE_TENANT, userId, "pltest" + finalI + "@example.com", null, + timeJoined); synchronized (lock) { allUserIds.add(userId); } @@ -146,7 +152,7 @@ private void createPasswordlessUsersWithEmail(Main main) throws Exception { } if (finalI % 10000 == 9999) { - System.out.println("Created " + ((finalI +1)) + " users"); + System.out.println("Created " + ((finalI + 1)) + " users"); } }); } @@ -176,7 +182,7 @@ private void createPasswordlessUsersWithPhone(Main main) throws Exception { } if (finalI % 10000 == 9999) { - System.out.println("Created " + ((finalI +1)) + " users"); + System.out.println("Created " + ((finalI + 1)) + " users"); } }); } @@ -198,7 +204,8 @@ private void createThirdpartyUsers(Main main) throws Exception { long timeJoined = System.currentTimeMillis(); try { - storage.signUp(TenantIdentifier.BASE_TENANT, userId, "tptest" + finalI + "@example.com", new LoginMethod.ThirdParty("google", "googleid" + finalI), timeJoined ); + storage.signUp(TenantIdentifier.BASE_TENANT, userId, "tptest" + finalI + "@example.com", + new LoginMethod.ThirdParty("google", "googleid" + finalI), timeJoined); synchronized (lock) { allUserIds.add(userId); } @@ -207,7 +214,7 @@ private void createThirdpartyUsers(Main main) throws Exception { } if (finalI % 10000 == 9999) { - System.out.println("Created " + (finalI +1) + " users"); + System.out.println("Created " + (finalI + 1) + " users"); } }); } @@ -383,6 +390,30 @@ private void createSessions(Main main) throws Exception { es.awaitTermination(10, TimeUnit.MINUTES); } + private void createActiveUserEntries(Main main) throws Exception { + System.out.println("Creating active user entries..."); + + ExecutorService es = Executors.newFixedThreadPool(NUM_THREADS); + + for (String userId : allPrimaryUserIds) { + String finalUserId = userId; + es.execute(() -> { + try { + Storage storage = StorageLayer.getBaseStorage(main); + Start start = (Start) storage; + + start.updateLastActive(new AppIdentifier(null, null), finalUserId, System.currentTimeMillis() - new Random().nextInt(1000 * 3600 * 24 * 60)); + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + es.shutdown(); + es.awaitTermination(10, TimeUnit.MINUTES); + } + @Test public void testCreatingOneMillionUsers() throws Exception { if (System.getenv("ONE_MILLION_USERS_TEST") == null) { @@ -397,7 +428,7 @@ public void testCreatingOneMillionUsers() throws Exception { FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA, EE_FEATURES.DASHBOARD_LOGIN}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -442,6 +473,13 @@ public void testCreatingOneMillionUsers() throws Exception { System.out.println("Time taken to create sessions: " + ((en - st) / 1000) + " sec"); } + { + long st = System.currentTimeMillis(); + createActiveUserEntries(process.getProcess()); + long en = System.currentTimeMillis(); + System.out.println("Time taken to create active user entries: " + ((en - st) / 1000) + " sec"); + } + sanityCheckAPIs(process.getProcess()); allUserIds.clear(); allPrimaryUserIds.clear(); @@ -463,7 +501,7 @@ public void testCreatingOneMillionUsers() throws Exception { FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA, EE_FEATURES.DASHBOARD_LOGIN}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -646,7 +684,8 @@ private void sanityCheckAPIs(Main main) throws Exception { JsonArray userRolesArr = response.getAsJsonArray("roles"); assertEquals(1, userRolesArr.size()); assertTrue( - userRolesArr.get(0).getAsString().equals("admin") || userRolesArr.get(0).getAsString().equals("user") + userRolesArr.get(0).getAsString().equals("admin") || + userRolesArr.get(0).getAsString().equals("user") ); } @@ -791,7 +830,8 @@ private void measureOperations(Main main) throws Exception { int finalI = i; es.execute(() -> { try { - ThirdParty.signInUp(main, "twitter", "twitterid" + finalI, "twitter" + finalI + "@example.com"); + ThirdParty.signInUp(main, "twitter", "twitterid" + finalI, + "twitter" + finalI + "@example.com"); } catch (Exception e) { errorCount.incrementAndGet(); throw new RuntimeException(e); @@ -817,7 +857,8 @@ private void measureOperations(Main main) throws Exception { int finalI = i; es.execute(() -> { try { - ThirdParty.signInUp(main, "twitter", "twitterid" + finalI, "twitter" + finalI + "@example.com"); + ThirdParty.signInUp(main, "twitter", "twitterid" + finalI, + "twitter" + finalI + "@example.com"); } catch (Exception e) { errorCount.incrementAndGet(); throw new RuntimeException(e); @@ -882,6 +923,36 @@ private void measureOperations(Main main) throws Exception { return null; }); System.out.println("Update user metadata " + time); + assert time < 3000; + } + + { // measure user counting + long time = measureTime(() -> { + try { + AuthRecipe.getUsersCount(main, null); + AuthRecipe.getUsersCount(main, new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}); + AuthRecipe.getUsersCount(main, new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("User counting: " + time); + assert time < 10000; + } + { // measure telemetry + long time = measureTime(() -> { + try { + FeatureFlag.getInstance(main).getPaidFeatureStats(); + } catch (Exception e) { + errorCount.incrementAndGet(); + throw new RuntimeException(e); + } + return null; + }); + System.out.println("Telemetry: " + time); + assert time < 6000; } assertEquals(0, errorCount.get()); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java b/src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java new file mode 100644 index 00000000..21a86f60 --- /dev/null +++ b/src/test/java/io/supertokens/storage/postgresql/test/PostgresSQLConfigTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package io.supertokens.storage.postgresql.test; + +import io.supertokens.ProcessState; +import io.supertokens.storage.postgresql.annotations.DashboardInfo; +import io.supertokens.storage.postgresql.config.PostgreSQLConfig; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.junit.Assert.*; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class PostgresSQLConfigTest { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testMatchConfigPropertiesDescription() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + // Skipping postgresql_config_version because it doesn't + // have a description in the config.yaml file + String[] ignoredProperties = {"postgresql_config_version"}; + + // Match the descriptions in the config.yaml file with the descriptions in the + // CoreConfig class + matchYamlAndConfigDescriptions("./config.yaml", ignoredProperties); + + // Match the descriptions in the devConfig.yaml file with the descriptions in + // the CoreConfig class + String[] devConfigIgnoredProperties = Arrays.copyOf(ignoredProperties, ignoredProperties.length + 2); + // We ignore these properties in devConfig.yaml because it has a different + // description + // in devConfig.yaml and has a default value + devConfigIgnoredProperties[ignoredProperties.length] = "postgresql_user"; + devConfigIgnoredProperties[ignoredProperties.length + 1] = "postgresql_password"; + matchYamlAndConfigDescriptions("./devConfig.yaml", devConfigIgnoredProperties); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private void matchYamlAndConfigDescriptions(String path, String[] ignoreProperties) throws Exception { + try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + // Get the content of the file as string + String content = reader.lines().collect(Collectors.joining(System.lineSeparator())); + // Find the line that contains 'postgresql_config_version', and then split + // the file after that line + String allProperties = content.split("postgresql_config_version:\\s*\\d+\n")[1]; + + // Split by all the other allProperties string by new line + String[] properties = allProperties.split("\n\n"); + // This will contain the description of each property from the yaml file + Map propertyDescriptions = new HashMap(); + + System.out.println("Last property: " + properties[properties.length - 1] + "\n\n"); + + for (int i = 0; i < properties.length; i++) { + String possibleProperty = properties[i].trim(); + String[] lines = possibleProperty.split("\n"); + // This ensures that it is a property with a description as a comment + // at the top + if (lines[lines.length - 1].endsWith(":")) { + String propertyKeyString = lines[lines.length - 1]; + // Remove the comment "# " from the start + String propertyKey = propertyKeyString.substring(2, propertyKeyString.length() - 1); + String propertyDescription = ""; + // Remove the comment "# " from the start and merge all the lines to form the + // description + for (int j = 0; j < lines.length - 1; j++) { + propertyDescription = propertyDescription + " " + lines[j].substring(2); + } + propertyDescription = propertyDescription.trim(); + + propertyDescriptions.put(propertyKey, propertyDescription); + } + } + + for (String fieldId : PostgreSQLConfig.getValidFields()) { + if (Arrays.asList(ignoreProperties).contains(fieldId)) { + continue; + } + + Field field = PostgreSQLConfig.class.getDeclaredField(fieldId); + + // Skip fields that are not annotated with JsonProperty + if (!field.isAnnotationPresent(JsonProperty.class)) { + continue; + } + + + String valueInfo = ""; + if (field.getType() == String.class) { + valueInfo = "string value."; + } else if (field.getType() == int.class || field.getType() == Integer.class) { + valueInfo = "integer value."; + } else if (field.getType() == long.class) { + valueInfo = "long value."; + } else if (field.getType() == boolean.class || field.getType() == Boolean.class) { + valueInfo = "boolean value."; + } + + String descriptionInConfig = field.getAnnotation(DashboardInfo.class).description(); + descriptionInConfig = "(DIFFERENT_ACROSS_TENANTS" + + (field.getAnnotation(DashboardInfo.class).isOptional() ? " | OPTIONAL" : " | COMPULSORY") + + (field.getAnnotation(DashboardInfo.class).isOptional() ? + " | Default: " + field.getAnnotation(DashboardInfo.class).defaultValue() : "") + + ") " + + valueInfo + " " + + descriptionInConfig; + String descriptionInYaml = propertyDescriptions.get(fieldId); + + if (descriptionInYaml == null) { + fail("Unable to find description or property for " + fieldId + " in " + path + " file"); + } + + // Assert that description in yaml contains the description in config + assertEquals("For " + fieldId, descriptionInYaml.trim(), descriptionInConfig.trim()); + } + } + } + +} diff --git a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java index 8f6d1699..e2ee74f2 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/StorageLayerTest.java @@ -115,13 +115,18 @@ public void testLinkedAccountUser() throws Exception { AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); Thread.sleep(50); - AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid", "test2@example.com").user; + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid", + "test2@example.com").user; Thread.sleep(50); - Passwordless.CreateCodeResponse code1 = Passwordless.createCode(process.getProcess(), "test3@example.com", null, null, null); - AuthRecipeUserInfo user3 = Passwordless.consumeCode(process.getProcess(), code1.deviceId, code1.deviceIdHash, code1.userInputCode, null).user; + Passwordless.CreateCodeResponse code1 = Passwordless.createCode(process.getProcess(), "test3@example.com", null, + null, null); + AuthRecipeUserInfo user3 = Passwordless.consumeCode(process.getProcess(), code1.deviceId, code1.deviceIdHash, + code1.userInputCode, null).user; Thread.sleep(50); - Passwordless.CreateCodeResponse code2 = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); - AuthRecipeUserInfo user4 = Passwordless.consumeCode(process.getProcess(), code2.deviceId, code2.deviceIdHash, code2.userInputCode, null).user; + Passwordless.CreateCodeResponse code2 = Passwordless.createCode(process.getProcess(), null, "+919876543210", + null, null); + AuthRecipeUserInfo user4 = Passwordless.consumeCode(process.getProcess(), code2.deviceId, code2.deviceIdHash, + code2.userInputCode, null).user; AuthRecipe.createPrimaryUser(process.getProcess(), user3.getSupertokensUserId()); AuthRecipe.linkAccounts(process.getProcess(), user1.getSupertokensUserId(), user3.getSupertokensUserId()); @@ -135,8 +140,9 @@ public void testLinkedAccountUser() throws Exception { user4.getSupertokensUserId() }; - for (String userId : userIds){ - AuthRecipeUserInfo primaryUser = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())).getPrimaryUserById( + for (String userId : userIds) { + AuthRecipeUserInfo primaryUser = ((AuthRecipeStorage) StorageLayer.getStorage( + process.getProcess())).getPrimaryUserById( new AppIdentifier(null, null), userId); assertEquals(user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); assertEquals(4, primaryUser.loginMethods.length); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java index 51673061..eb135240 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/SuperTokensSaaSSecretTest.java @@ -86,11 +86,12 @@ public void testThatTenantCannotSetDatabaseRelatedConfigIfSuperTokensSaaSSecretI try { JsonObject j = new JsonObject(); j.addProperty(PROTECTED_DB_CONFIG[i], ""); - Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), - new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), - new PasswordlessConfig(false), - null, null, - j), true); + Multitenancy.addNewOrUpdateAppOrTenant(process.main, + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + null, null, + j), true); fail(); } catch (BadPermissionException e) { assertEquals(e.getMessage(), "Not allowed to modify DB related configs."); @@ -162,12 +163,13 @@ public void testThatTenantCanSetDatabaseRelatedConfigIfSuperTokensSaaSSecretIsNo } else if (PROTECTED_DB_CONFIG_VALUES[i] instanceof Integer) { j.addProperty(PROTECTED_DB_CONFIG[i], (Integer) PROTECTED_DB_CONFIG_VALUES[i]); } - Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), - new EmailPasswordConfig(false), - new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), - new PasswordlessConfig(false), - null, null, - j), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.main, + new TenantConfig(new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + null, null, + j), false); } JsonObject coreConfig = new JsonObject(); @@ -224,7 +226,8 @@ public void testThatTenantCannotGetDatabaseRelatedConfigIfSuperTokensSaaSSecretI { JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", - HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, "/recipe/multitenancy/tenant/list"), + HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, + "/recipe/multitenancy/tenant/list"), null, 1000, 1000, null, SemVer.v3_0.get(), "GET", apiKey, "multitenancy"); @@ -245,7 +248,8 @@ public void testThatTenantCannotGetDatabaseRelatedConfigIfSuperTokensSaaSSecretI { JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", - HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, "/recipe/multitenancy/tenant/list"), + HttpRequestForTesting.getMultitenantUrl(TenantIdentifier.BASE_TENANT, + "/recipe/multitenancy/tenant/list"), null, 1000, 1000, null, SemVer.v3_0.get(), "GET", saasSecret, "multitenancy"); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/TableCreationTest.java b/src/test/java/io/supertokens/storage/postgresql/test/TableCreationTest.java index f215a4c0..81f989c8 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/TableCreationTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/TableCreationTest.java @@ -45,7 +45,7 @@ public void beforeEach() { @Test public void checkingCreationOfNewTable() throws InterruptedException { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/TestMainThread.java b/src/test/java/io/supertokens/storage/postgresql/test/TestMainThread.java index 5dec4454..4c3f6e25 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/TestMainThread.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/TestMainThread.java @@ -75,8 +75,9 @@ public void testThatMainThreadIsSameThroughout() throws Exception { JsonObject requestBody = new JsonObject(); requestBody.addProperty("appId", "a1"); requestBody.add("coreConfig", config); - HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", "http://localhost:3567/recipe/multitenancy/app", - requestBody, 1000, 2500, null, "3.0", "multitenancy"); + HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/multitenancy/app", + requestBody, 1000, 2500, null, "3.0", "multitenancy"); Storage storage2 = StorageLayer.getStorage(new TenantIdentifier(null, "a1", null), process.getProcess()); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/httpRequest/HttpRequestForTesting.java b/src/test/java/io/supertokens/storage/postgresql/test/httpRequest/HttpRequestForTesting.java index 5ca0aa3e..52e1f7cf 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/httpRequest/HttpRequestForTesting.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/httpRequest/HttpRequestForTesting.java @@ -62,7 +62,8 @@ private static boolean isJsonValid(String jsonInString) { @SuppressWarnings("unchecked") public static T sendGETRequest(Main main, String requestID, String url, Map params, - int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String rid) + int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, + String rid) throws IOException, HttpResponseException { StringBuilder paramBuilder = new StringBuilder(); @@ -198,101 +199,107 @@ public static T sendJsonRequest(Main main, String requestID, String url, Jso } public static T sendJsonPOSTRequest(Main main, String requestID, String url, JsonElement requestBody, - int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String rid) + int connectionTimeoutMS, int readTimeoutMS, Integer version, + String cdiVersion, String rid) throws IOException, HttpResponseException { return sendJsonRequest(main, requestID, url, requestBody, connectionTimeoutMS, readTimeoutMS, version, cdiVersion, "POST", null, rid); } public static T sendJsonPOSTRequest(Main main, String requestID, String url, JsonElement requestBody, - int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String apiKey, String rid) + int connectionTimeoutMS, int readTimeoutMS, Integer version, + String cdiVersion, String apiKey, String rid) throws IOException, HttpResponseException { return sendJsonRequest(main, requestID, url, requestBody, connectionTimeoutMS, readTimeoutMS, version, cdiVersion, "POST", apiKey, rid); } public static T sendJsonPUTRequest(Main main, String requestID, String url, JsonElement requestBody, - int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String rid) + int connectionTimeoutMS, int readTimeoutMS, Integer version, + String cdiVersion, String rid) throws IOException, HttpResponseException { return sendJsonRequest(main, requestID, url, requestBody, connectionTimeoutMS, readTimeoutMS, version, cdiVersion, "PUT", null, rid); } public static T sendJsonDELETERequest(Main main, String requestID, String url, JsonElement requestBody, - int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String rid) + int connectionTimeoutMS, int readTimeoutMS, Integer version, + String cdiVersion, String rid) throws IOException, HttpResponseException { return sendJsonRequest(main, requestID, url, requestBody, connectionTimeoutMS, readTimeoutMS, version, cdiVersion, "DELETE", null, rid); } @SuppressWarnings("unchecked") - public static T sendJsonDELETERequestWithQueryParams(Main main, String requestID, String url, Map params, - int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String rid) + public static T sendJsonDELETERequestWithQueryParams(Main main, String requestID, String url, + Map params, + int connectionTimeoutMS, int readTimeoutMS, + Integer version, String cdiVersion, String rid) throws IOException, HttpResponseException { - StringBuilder paramBuilder = new StringBuilder(); + StringBuilder paramBuilder = new StringBuilder(); - if (params != null) { - for (Map.Entry entry : params.entrySet()) { - paramBuilder.append(entry.getKey()).append("=") - .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).append("&"); - } - } - String paramsStr = paramBuilder.toString(); - if (!paramsStr.equals("")) { - paramsStr = paramsStr.substring(0, paramsStr.length() - 1); - url = url + "?" + paramsStr; + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + paramBuilder.append(entry.getKey()).append("=") + .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).append("&"); + } + } + String paramsStr = paramBuilder.toString(); + if (!paramsStr.equals("")) { + paramsStr = paramsStr.substring(0, paramsStr.length() - 1); + url = url + "?" + paramsStr; + } + URL obj = getURL(main, requestID, url); + InputStream inputStream = null; + HttpURLConnection con = null; + + try { + con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("DELETE"); + con.setConnectTimeout(connectionTimeoutMS); + con.setReadTimeout(readTimeoutMS + 1000); + if (version != null) { + con.setRequestProperty("api-version", version + ""); + } + if (cdiVersion != null) { + con.setRequestProperty("cdi-version", cdiVersion); + } + if (rid != null) { + con.setRequestProperty("rId", rid); + } + + int responseCode = con.getResponseCode(); + + if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { + inputStream = con.getInputStream(); + } else { + inputStream = con.getErrorStream(); + } + + StringBuilder response = new StringBuilder(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String inputLine; + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); } - URL obj = getURL(main, requestID, url); - InputStream inputStream = null; - HttpURLConnection con = null; - - try { - con = (HttpURLConnection) obj.openConnection(); - con.setRequestMethod("DELETE"); - con.setConnectTimeout(connectionTimeoutMS); - con.setReadTimeout(readTimeoutMS + 1000); - if (version != null) { - con.setRequestProperty("api-version", version + ""); - } - if (cdiVersion != null) { - con.setRequestProperty("cdi-version", cdiVersion); - } - if (rid != null) { - con.setRequestProperty("rId", rid); - } - - int responseCode = con.getResponseCode(); - - if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { - inputStream = con.getInputStream(); - } else { - inputStream = con.getErrorStream(); - } - - StringBuilder response = new StringBuilder(); - try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - String inputLine; - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - } - if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { - if (!isJsonValid(response.toString())) { - return (T) response.toString(); - } - return (T) (new JsonParser().parse(response.toString())); - } - throw new HttpResponseException(responseCode, response.toString()); - } finally { - if (inputStream != null) { - inputStream.close(); - } - - if (con != null) { - con.disconnect(); - } + } + if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { + if (!isJsonValid(response.toString())) { + return (T) response.toString(); } + return (T) (new JsonParser().parse(response.toString())); + } + throw new HttpResponseException(responseCode, response.toString()); + } finally { + if (inputStream != null) { + inputStream.close(); + } + + if (con != null) { + con.disconnect(); + } } + } public static String getMultitenantUrl(TenantIdentifier tenantIdentifier, String path) { StringBuilder sb = new StringBuilder(); diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java index 97a3fb6b..dc592a21 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/StorageLayerTest.java @@ -49,7 +49,6 @@ import java.io.IOException; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -175,11 +174,12 @@ public void storageInstanceIsReusedAcrossTenants() StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( - Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), + Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()) + .getAccessTokenValidityInMillis(), (long) 3600 * 1000); Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, "abc", null), process.getProcess()) - .getAccessTokenValidity(), + .getAccessTokenValidityInMillis(), (long) 3601 * 1000); Assert.assertEquals( @@ -239,11 +239,12 @@ public void storageInstanceIsReusedAcrossTenantsComplex() StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())); Assert.assertEquals( - Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), + Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()) + .getAccessTokenValidityInMillis(), (long) 3600 * 1000); Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, "abc", null), process.getProcess()) - .getAccessTokenValidity(), + .getAccessTokenValidityInMillis(), (long) 3601 * 1000); Assert.assertEquals( @@ -329,7 +330,8 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() fail(); } catch (InvalidConfigException e) { assertEquals(e.getMessage(), - "You cannot set different values for postgresql_thirdparty_users_table_name for the same user pool"); + "You cannot set different values for postgresql_thirdparty_users_table_name for the same user " + + "pool"); } process.kill(); @@ -454,11 +456,12 @@ public void newStorageIsNotCreatedWhenSameTenantIsAdded() existingStorage); Assert.assertEquals( - Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()).getAccessTokenValidity(), + Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()) + .getAccessTokenValidityInMillis(), (long) 3600 * 1000); Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, "abc", null), process.getProcess()) - .getAccessTokenValidity(), + .getAccessTokenValidityInMillis(), (long) 3601 * 1000); Assert.assertEquals( @@ -806,7 +809,8 @@ public void testTenantCreationAndThenDbDownDbThrowsErrorInRecipesAndDoesntAffect tenantConfigJson); StorageLayer.getMultitenancyStorage(process.getProcess()).createTenant(tenantConfig); - MultitenancyHelper.getInstance(process.getProcess()).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); + MultitenancyHelper.getInstance(process.getProcess()) + .refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); try { EmailPassword.signIn(tid, (StorageLayer.getStorage(tid, process.getProcess())), diff --git a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestForNoCrashDuringStartup.java b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestForNoCrashDuringStartup.java index b8635224..5081c414 100644 --- a/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestForNoCrashDuringStartup.java +++ b/src/test/java/io/supertokens/storage/postgresql/test/multitenancy/TestForNoCrashDuringStartup.java @@ -107,24 +107,27 @@ public void testThatCUDRecoversWhenItFailsToAddEntryDuringCreation() throws Exce assertEquals(2, allTenants.length); // should have the new CUD try { - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); fail(); } catch (HttpResponseException e) { // ignore - assertTrue(e.getMessage().contains("Internal Error")); // retried creating tenant entry + assertEquals("Http error. Status Code: 500. Message: java.sql.SQLException: Simulated error in addTenantIdInTargetStorage", e.getMessage()); } MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); } @Test public void testThatCUDRecoversWhenTheDbIsDownDuringCreationButDbComesUpLater() throws Exception { Start start = ((Start) StorageLayer.getBaseStorage(process.getProcess())); try { - update(start, "DROP DATABASE st5000;", pst -> {}); + update(start, "DROP DATABASE st5000;", pst -> { + }); } catch (Exception e) { // ignore } @@ -147,24 +150,30 @@ public void testThatCUDRecoversWhenTheDbIsDownDuringCreationButDbComesUpLater() fail(); } catch (StorageQueryException e) { // ignore - assertEquals("java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database \"st5000\" does not exist", e.getMessage()); + assertEquals( + "java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + + "initialize pool: FATAL: database \"st5000\" does not exist", + e.getMessage()); } TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); assertEquals(2, allTenants.length); // should have the new CUD try { - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); fail(); } catch (HttpResponseException e) { // ignore - assertTrue(e.getMessage().contains("Internal Error")); // db is still down + assertEquals("Http error. Status Code: 500. Message: java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database \"st5000\" does not exist", e.getMessage()); } - update(start, "CREATE DATABASE st5000;", pst -> {}); + update(start, "CREATE DATABASE st5000;", pst -> { + }); // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); } @Test @@ -260,7 +269,8 @@ public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddEntry MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); } @Test @@ -291,7 +301,8 @@ public void testThatCoreDoesNotCrashDuringStartupWhenTenantEntryIsInconsistentIn assertEquals(2, allTenants.length); // should have the new CUD Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); - update(start, "DELETE FROM apps;", pst -> {}); + update(start, "DELETE FROM apps;", pst -> { + }); process.kill(false); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -306,13 +317,15 @@ public void testThatCoreDoesNotCrashDuringStartupWhenTenantEntryIsInconsistentIn MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); Session.createNewSession(process.getProcess(), "userid", new JsonObject(), new JsonObject()); } @Test - public void testThatCoreDoesNotCrashDuringStartupWhenAppCreationFailedToAddEntryInTheBaseTenantStorage() throws Exception { + public void testThatCoreDoesNotCrashDuringStartupWhenAppCreationFailedToAddEntryInTheBaseTenantStorage() + throws Exception { JsonObject coreConfig = new JsonObject(); TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); @@ -354,7 +367,8 @@ public void testThatCoreDoesNotCrashDuringStartupWhenAppCreationFailedToAddEntry assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); Session.createNewSession( new TenantIdentifier(null, "a1", null), @@ -364,7 +378,8 @@ public void testThatCoreDoesNotCrashDuringStartupWhenAppCreationFailedToAddEntry } @Test - public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddTenantEntryInTargetStorageWithLoadOnlyCUDConfig() throws Exception { + public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddTenantEntryInTargetStorageWithLoadOnlyCUDConfig() + throws Exception { JsonObject coreConfig = new JsonObject(); TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); @@ -442,10 +457,12 @@ public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddTenan assertEquals(2, allTenants.length); // should have the new CUD // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); try { - tpSignInUpAndGetResponse(new TenantIdentifier("localhost", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("localhost", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); fail(); } catch (HttpResponseException e) { // ignore @@ -465,9 +482,11 @@ public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddTenan assertEquals(2, allTenants.length); // should have the new CUD // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("localhost", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("localhost", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); try { - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); fail(); } catch (HttpResponseException e) { // ignore @@ -488,7 +507,8 @@ public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddTenan public void testThatTenantComesToLifeOnceTheTargetDbIsUpAfterCoreRestart() throws Exception { Start start = ((Start) StorageLayer.getBaseStorage(process.getProcess())); try { - update(start, "DROP DATABASE st5000;", pst -> {}); + update(start, "DROP DATABASE st5000;", pst -> { + }); } catch (Exception e) { // ignore } @@ -511,18 +531,22 @@ public void testThatTenantComesToLifeOnceTheTargetDbIsUpAfterCoreRestart() throw fail(); } catch (StorageQueryException e) { // ignore - assertEquals("java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database \"st5000\" does not exist", e.getMessage()); + assertEquals( + "java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to " + + "initialize pool: FATAL: database \"st5000\" does not exist", + e.getMessage()); } TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); assertEquals(2, allTenants.length); // should have the new CUD try { - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); fail(); } catch (HttpResponseException e) { // ignore - assertTrue(e.getMessage().contains("Internal Error")); // db is still down + assertEquals("Http error. Status Code: 500. Message: java.sql.SQLException: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to initialize pool: FATAL: database \"st5000\" does not exist", e.getMessage()); } process.kill(false); @@ -537,13 +561,16 @@ public void testThatTenantComesToLifeOnceTheTargetDbIsUpAfterCoreRestart() throw // the process should start successfully even though the db is down start = ((Start) StorageLayer.getBaseStorage(process.getProcess())); - update(start, "CREATE DATABASE st5000;", pst -> {}); + update(start, "CREATE DATABASE st5000;", pst -> { + }); // this should succeed now - tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", + "test@example.com", process.getProcess(), SemVer.v5_0); } - public static JsonObject tpSignInUpAndGetResponse(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email, Main main, SemVer version) + public static JsonObject tpSignInUpAndGetResponse(TenantIdentifier tenantIdentifier, String thirdPartyId, + String thirdPartyUserId, String email, Main main, SemVer version) throws HttpResponseException, IOException { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", email);