From 9c49016f97117c82d3bc93b6303b4656fd61ee4d Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 27 Jul 2023 18:29:59 +0530 Subject: [PATCH] feat: Add multitenancy for dashboard recipe --- .../recipe/dashboard/api/__init__.py | 2 + .../recipe/dashboard/api/analytics.py | 5 ++- .../recipe/dashboard/api/api_key_protector.py | 7 +++- .../recipe/dashboard/api/list_tenants.py | 39 +++++++++++++++++++ .../recipe/dashboard/api/search/getTags.py | 2 +- .../recipe/dashboard/api/signout.py | 5 ++- .../dashboard/api/userdetails/user_delete.py | 5 ++- .../api/userdetails/user_email_verify_get.py | 5 ++- .../api/userdetails/user_email_verify_put.py | 9 +++-- .../user_email_verify_token_post.py | 9 +++-- .../dashboard/api/userdetails/user_get.py | 5 ++- .../api/userdetails/user_metadata_get.py | 5 ++- .../api/userdetails/user_metadata_put.py | 5 ++- .../api/userdetails/user_password_put.py | 10 +++-- .../dashboard/api/userdetails/user_put.py | 5 ++- .../api/userdetails/user_sessions_get.py | 9 ++++- .../api/userdetails/user_sessions_post.py | 5 ++- .../recipe/dashboard/api/users_count_get.py | 7 +++- .../recipe/dashboard/api/users_get.py | 2 + .../recipe/dashboard/constants.py | 1 + .../recipe/dashboard/interfaces.py | 11 ++++++ supertokens_python/recipe/dashboard/recipe.py | 16 ++++++-- supertokens_python/recipe/dashboard/utils.py | 4 +- .../recipe/emailpassword/asyncio/__init__.py | 10 ++--- .../recipe/emailpassword/syncio/__init__.py | 20 +++++----- .../recipe/multitenancy/interfaces.py | 36 ++++++++++++++++- .../multitenancy/recipe_implementation.py | 15 +++++-- .../recipe/session/access_token.py | 4 +- .../recipe/session/asyncio/__init__.py | 11 ++++-- .../recipe/session/interfaces.py | 24 ++++++++++-- .../recipe/session/recipe_implementation.py | 26 +++++++++---- .../recipe/session/session_class.py | 2 +- .../recipe/session/session_functions.py | 32 ++++++++------- .../recipe/session/syncio/__init__.py | 12 ++++-- .../recipe/thirdparty/api/implementation.py | 2 +- supertokens_python/supertokens.py | 20 ++++++++-- supertokens_python/types.py | 3 ++ 37 files changed, 301 insertions(+), 89 deletions(-) create mode 100644 supertokens_python/recipe/dashboard/api/list_tenants.py diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index 3ae3a869a..067fa84ec 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -31,6 +31,7 @@ from .users_count_get import handle_users_count_get_api from .users_get import handle_users_get_api from .validate_key import handle_validate_key_api +from .list_tenants import handle_list_tenants_api __all__ = [ "handle_dashboard_api", @@ -53,4 +54,5 @@ "handle_emailpassword_signout_api", "handle_get_tags", "handle_analytics_post", + "handle_list_tenants_api", ] diff --git a/supertokens_python/recipe/dashboard/api/analytics.py b/supertokens_python/recipe/dashboard/api/analytics.py index b467299e2..f22bfb015 100644 --- a/supertokens_python/recipe/dashboard/api/analytics.py +++ b/supertokens_python/recipe/dashboard/api/analytics.py @@ -35,7 +35,10 @@ async def handle_analytics_post( - _: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any] + _: APIInterface, + _tenant_id: str, + api_options: APIOptions, + _user_context: Dict[str, Any], ) -> AnalyticsResponse: if not Supertokens.get_instance().telemetry: return AnalyticsResponse() diff --git a/supertokens_python/recipe/dashboard/api/api_key_protector.py b/supertokens_python/recipe/dashboard/api/api_key_protector.py index 0ea448881..bceaba269 100644 --- a/supertokens_python/recipe/dashboard/api/api_key_protector.py +++ b/supertokens_python/recipe/dashboard/api/api_key_protector.py @@ -32,9 +32,10 @@ async def api_key_protector( api_implementation: APIInterface, + tenant_id: str, api_options: APIOptions, api_function: Callable[ - [APIInterface, APIOptions, Dict[str, Any]], Awaitable[APIResponse] + [APIInterface, str, APIOptions, Dict[str, Any]], Awaitable[APIResponse] ], user_context: Dict[str, Any], ) -> Optional[BaseResponse]: @@ -47,5 +48,7 @@ async def api_key_protector( "Unauthorised access", 401, api_options.response ) - response = await api_function(api_implementation, api_options, user_context) + response = await api_function( + api_implementation, tenant_id, api_options, user_context + ) return send_200_response(response.to_json(), api_options.response) diff --git a/supertokens_python/recipe/dashboard/api/list_tenants.py b/supertokens_python/recipe/dashboard/api/list_tenants.py new file mode 100644 index 000000000..013933c99 --- /dev/null +++ b/supertokens_python/recipe/dashboard/api/list_tenants.py @@ -0,0 +1,39 @@ +# Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict + +if TYPE_CHECKING: + from supertokens_python.recipe.dashboard.interfaces import ( + APIOptions, + APIInterface, + ) + from supertokens_python.types import APIResponse + +from supertokens_python.recipe.multitenancy.asyncio import list_all_tenants +from supertokens_python.recipe.dashboard.interfaces import ( + DashboardListTenantsGetResponse, +) + + +async def handle_list_tenants_api( + _api_implementation: APIInterface, + _tenant_id: str, + _api_options: APIOptions, + user_context: Dict[str, Any], +) -> APIResponse: + tenants = await list_all_tenants(user_context) + return DashboardListTenantsGetResponse(tenants.tenants) diff --git a/supertokens_python/recipe/dashboard/api/search/getTags.py b/supertokens_python/recipe/dashboard/api/search/getTags.py index 20598c62a..fd034d88d 100644 --- a/supertokens_python/recipe/dashboard/api/search/getTags.py +++ b/supertokens_python/recipe/dashboard/api/search/getTags.py @@ -24,7 +24,7 @@ async def handle_get_tags( - _: APIInterface, __: APIOptions, _user_context: Dict[str, Any] + _: APIInterface, _tenant_id: str, __: APIOptions, _user_context: Dict[str, Any] ) -> SearchTagsOK: response = await Querier.get_instance().send_get_request( NormalisedURLPath("/user/search/tags") diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py index 136441f1b..b4b29a191 100644 --- a/supertokens_python/recipe/dashboard/api/signout.py +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -26,7 +26,10 @@ async def handle_emailpassword_signout_api( - _: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any] + _: APIInterface, + _tenant_id: str, + api_options: APIOptions, + _user_context: Dict[str, Any], ) -> SignOutOK: if api_options.config.auth_mode == "api-key": return SignOutOK() diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_delete.py b/supertokens_python/recipe/dashboard/api/userdetails/user_delete.py index 62a5f4281..8f7f2d74a 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_delete.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_delete.py @@ -6,7 +6,10 @@ async def handle_user_delete( - _api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + _user_context: Dict[str, Any], ) -> UserDeleteAPIResponse: user_id = api_options.request.get_query_param("userId") diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_get.py b/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_get.py index c9caa2f23..54e0c2a9e 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_get.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_get.py @@ -12,7 +12,10 @@ async def handle_user_email_verify_get( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> Union[UserEmailVerifyGetAPIResponse, FeatureNotEnabledError]: req = api_options.request user_id = req.get_query_param("userId") diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_put.py b/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_put.py index f26971a78..414407056 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_put.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_put.py @@ -19,7 +19,10 @@ async def handle_user_email_verify_put( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> UserEmailVerifyPutAPIResponse: request_body: Dict[str, Any] = await api_options.request.json() # type: ignore user_id = request_body.get("userId") @@ -37,7 +40,7 @@ async def handle_user_email_verify_put( if verified: token_response = await create_email_verification_token( - user_id, user_context=user_context + user_id, tenant_id=tenant_id, user_context=user_context ) if isinstance( @@ -46,7 +49,7 @@ async def handle_user_email_verify_put( return UserEmailVerifyPutAPIResponse() verify_response = await verify_email_using_token( - token_response.token, user_context=user_context + token_response.token, tenant_id, user_context=user_context ) if isinstance(verify_response, VerifyEmailUsingTokenInvalidTokenError): diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_token_post.py b/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_token_post.py index 65d522a9c..30534283a 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_token_post.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_token_post.py @@ -28,7 +28,10 @@ async def handle_email_verify_token_post( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> Union[ UserEmailVerifyTokenPostAPIOkResponse, UserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse, @@ -49,7 +52,7 @@ async def handle_email_verify_token_post( raise Exception("Should not come here") email_verification_token = await create_email_verification_token( - user_id, user_context=user_context + user_id, tenant_id=tenant_id, user_context=user_context ) if isinstance( @@ -59,8 +62,6 @@ async def handle_email_verify_token_post( assert isinstance(email_verification_token, CreateEmailVerificationTokenOkResult) - # TODO: Pass tenant id - tenant_id = "pass-tenant-id" email_verify_link = get_email_verify_link( api_options.app_info, email_verification_token.token, user_id, tenant_id ) diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_get.py b/supertokens_python/recipe/dashboard/api/userdetails/user_get.py index 8d224a12e..0d8350b0b 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_get.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_get.py @@ -16,7 +16,10 @@ async def handle_user_get( - _api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + _user_context: Dict[str, Any], ) -> Union[ UserGetAPINoUserFoundError, UserGetAPIOkResponse, diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_get.py b/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_get.py index 381a67125..b9033c84c 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_get.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_get.py @@ -11,7 +11,10 @@ async def handle_metadata_get( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> Union[UserMetadataGetAPIOkResponse, FeatureNotEnabledError]: user_id = api_options.request.get_query_param("userId") diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_put.py b/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_put.py index ab6c834d3..653c077af 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_put.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_metadata_put.py @@ -12,7 +12,10 @@ async def handle_metadata_put( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> UserMetadataPutAPIResponse: request_body: Dict[str, Any] = await api_options.request.json() # type: ignore user_id = request_body.get("userId") diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_password_put.py b/supertokens_python/recipe/dashboard/api/userdetails/user_password_put.py index beb6a20a1..b0bdd584d 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_password_put.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_password_put.py @@ -37,7 +37,10 @@ async def handle_user_password_put( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> Union[UserPasswordPutAPIResponse, UserPasswordPutAPIInvalidPasswordErrorResponse]: request_body: Dict[str, Any] = await api_options.request.json() # type: ignore user_id = request_body.get("userId") @@ -100,9 +103,8 @@ async def reset_password( password_validation_error ) - # TODO: Pass tenant id password_reset_token = await create_reset_password_token( - "pass-tenant-id", user_id, user_context + user_id, tenant_id, user_context ) if isinstance(password_reset_token, CreateResetPasswordWrongUserIdError): @@ -111,7 +113,7 @@ async def reset_password( raise Exception("Should never come here") password_reset_response = await reset_password_using_token( - "pass-tenant-id", password_reset_token.token, new_password, user_context + password_reset_token.token, new_password, tenant_id, user_context ) if isinstance( diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_put.py b/supertokens_python/recipe/dashboard/api/userdetails/user_put.py index 000028159..2e245bda9 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_put.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_put.py @@ -253,7 +253,10 @@ async def update_phone_for_recipe_id( async def handle_user_put( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> Union[ UserPutAPIOkResponse, UserPutAPIInvalidEmailErrorResponse, diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_get.py b/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_get.py index 7d5aa8e9c..6c78fc34d 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_get.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_get.py @@ -16,14 +16,19 @@ async def handle_sessions_get( - _api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], ) -> UserSessionsGetAPIResponse: user_id = api_options.request.get_query_param("userId") if user_id is None: raise_bad_input_exception("Missing required parameter 'userId'") - session_handles = await get_all_session_handles_for_user(user_id, user_context) + session_handles = await get_all_session_handles_for_user( + user_id, None, user_context + ) sessions: List[Optional[SessionInfo]] = [None for _ in session_handles] async def call_(i: int, session_handle: str): diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_post.py b/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_post.py index cad0c2020..cdb23ded8 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_post.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_sessions_post.py @@ -6,7 +6,10 @@ async def handle_user_sessions_post( - _api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any] + _api_interface: APIInterface, + _tenant_id: str, + api_options: APIOptions, + _user_context: Dict[str, Any], ) -> UserSessionsPostAPIResponse: request_body = await api_options.request.json() # type: ignore session_handles: Optional[List[str]] = request_body.get("sessionHandles") # type: ignore diff --git a/supertokens_python/recipe/dashboard/api/users_count_get.py b/supertokens_python/recipe/dashboard/api/users_count_get.py index cb0bb1aa5..dbbd15bd6 100644 --- a/supertokens_python/recipe/dashboard/api/users_count_get.py +++ b/supertokens_python/recipe/dashboard/api/users_count_get.py @@ -26,7 +26,10 @@ async def handle_users_count_get_api( - _: APIInterface, _api_options: APIOptions, _user_context: Dict[str, Any] + _: APIInterface, + tenant_id: str, + _api_options: APIOptions, + _user_context: Dict[str, Any], ) -> UserCountGetAPIResponse: - count = await Supertokens.get_instance().get_user_count(include_recipe_ids=None) + count = await Supertokens.get_instance().get_user_count(None, tenant_id) return UserCountGetAPIResponse(count=count) diff --git a/supertokens_python/recipe/dashboard/api/users_get.py b/supertokens_python/recipe/dashboard/api/users_get.py index 1bd8b5670..bb6eff621 100644 --- a/supertokens_python/recipe/dashboard/api/users_get.py +++ b/supertokens_python/recipe/dashboard/api/users_get.py @@ -35,6 +35,7 @@ async def handle_users_get_api( api_implementation: APIInterface, + tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ) -> APIResponse: @@ -58,6 +59,7 @@ async def handle_users_get_api( pagination_token=pagination_token, include_recipe_ids=None, query=api_options.request.get_query_params(), + tenant_id=tenant_id, ) # user metadata bulk fetch with batches: diff --git a/supertokens_python/recipe/dashboard/constants.py b/supertokens_python/recipe/dashboard/constants.py index 5f402449c..1f5b64c13 100644 --- a/supertokens_python/recipe/dashboard/constants.py +++ b/supertokens_python/recipe/dashboard/constants.py @@ -12,3 +12,4 @@ EMAIL_PASSSWORD_SIGNOUT = "/api/signout" SEARCH_TAGS_API = "/api/search/tags" DASHBOARD_ANALYTICS_API = "/api/analytics" +TENANTS_LIST_API = "/api/tenants/list" diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 02104e0e5..f639aa8e4 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -101,6 +101,17 @@ def to_json(self) -> Dict[str, Any]: } +from supertokens_python.recipe.multitenancy.interfaces import ListAllTenantsOkResult + + +class DashboardListTenantsGetResponse(APIResponse, ListAllTenantsOkResult): + def to_json(self): + return { + "status": self.status, + "tenants": [t.to_json() for t in self.tenants], + } + + class UserCountGetAPIResponse(APIResponse): status: str = "OK" diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index a53f39a81..4ab47c5df 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -41,6 +41,7 @@ handle_users_count_get_api, handle_users_get_api, handle_validate_key_api, + handle_list_tenants_api, ) from .api.implementation import APIImplementation from .exceptions import SuperTokensDashboardError @@ -72,6 +73,7 @@ USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API, + TENANTS_LIST_API, ) from .utils import ( InputOverrideConfig, @@ -129,7 +131,7 @@ def get_apis_handled(self) -> List[APIHandled]: async def handle_api_request( self, request_id: str, - tenant_id: Optional[str], + tenant_id: str, request: BaseRequest, path: NormalisedURLPath, method: str, @@ -160,7 +162,9 @@ async def handle_api_request( # Do API key validation for the remaining APIs api_function: Optional[ - Callable[[APIInterface, APIOptions, Dict[str, Any]], Awaitable[APIResponse]] + Callable[ + [APIInterface, str, APIOptions, Dict[str, Any]], Awaitable[APIResponse] + ] ] = None if request_id == USERS_LIST_GET_API: api_function = handle_users_get_api @@ -199,10 +203,16 @@ async def handle_api_request( elif request_id == DASHBOARD_ANALYTICS_API: if method == "post": api_function = handle_analytics_post + elif request_id == TENANTS_LIST_API: + api_function = handle_list_tenants_api if api_function is not None: return await api_key_protector( - self.api_implementation, api_options, api_function, user_context + self.api_implementation, + tenant_id, + api_options, + api_function, + user_context, ) return None diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 5a9ba5c19..bc4c9b5a3 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -13,7 +13,7 @@ # under the License. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, List if TYPE_CHECKING: from supertokens_python.framework.request import BaseRequest @@ -76,6 +76,7 @@ class UserWithMetadata: tp_info: Optional[Dict[str, Any]] = None first_name: Optional[str] = None last_name: Optional[str] = None + tenant_ids: List[str] def from_user( self, @@ -94,6 +95,7 @@ def from_user( self.tp_info = ( None if user.third_party_info is None else user.third_party_info.__dict__ ) + self.tenant_ids = user.tenant_ids return self diff --git a/supertokens_python/recipe/emailpassword/asyncio/__init__.py b/supertokens_python/recipe/emailpassword/asyncio/__init__.py index e96b4a33f..80d18dece 100644 --- a/supertokens_python/recipe/emailpassword/asyncio/__init__.py +++ b/supertokens_python/recipe/emailpassword/asyncio/__init__.py @@ -46,8 +46,8 @@ async def get_user_by_id( async def get_user_by_email( - tenant_id: Optional[str], email: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ) -> Union[User, None]: if user_context is None: @@ -58,8 +58,8 @@ async def get_user_by_email( async def create_reset_password_token( - tenant_id: Optional[str], user_id: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): if user_context is None: @@ -70,9 +70,9 @@ async def create_reset_password_token( async def reset_password_using_token( - tenant_id: Optional[str], token: str, new_password: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): if user_context is None: @@ -83,9 +83,9 @@ async def reset_password_using_token( async def sign_in( - tenant_id: Optional[str], email: str, password: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): if user_context is None: @@ -96,9 +96,9 @@ async def sign_in( async def sign_up( - tenant_id: Optional[str], email: str, password: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): if user_context is None: diff --git a/supertokens_python/recipe/emailpassword/syncio/__init__.py b/supertokens_python/recipe/emailpassword/syncio/__init__.py index 7328a8b39..a8bf2b4f4 100644 --- a/supertokens_python/recipe/emailpassword/syncio/__init__.py +++ b/supertokens_python/recipe/emailpassword/syncio/__init__.py @@ -44,31 +44,31 @@ def get_user_by_id( def get_user_by_email( - tenant_id: Optional[str], email: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ) -> Union[None, User]: from supertokens_python.recipe.emailpassword.asyncio import get_user_by_email - return sync(get_user_by_email(tenant_id, email, user_context)) + return sync(get_user_by_email(email, tenant_id, user_context)) def create_reset_password_token( - tenant_id: Optional[str], user_id: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): from supertokens_python.recipe.emailpassword.asyncio import ( create_reset_password_token, ) - return sync(create_reset_password_token(tenant_id, user_id, user_context)) + return sync(create_reset_password_token(user_id, tenant_id, user_context)) def reset_password_using_token( - tenant_id: Optional[str], token: str, new_password: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): from supertokens_python.recipe.emailpassword.asyncio import ( @@ -76,30 +76,30 @@ def reset_password_using_token( ) return sync( - reset_password_using_token(tenant_id, token, new_password, user_context) + reset_password_using_token(token, new_password, tenant_id, user_context) ) def sign_in( - tenant_id: Optional[str], email: str, password: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ) -> Union[SignInOkResult, SignInWrongCredentialsError]: from supertokens_python.recipe.emailpassword.asyncio import sign_in - return sync(sign_in(tenant_id, email, password, user_context)) + return sync(sign_in(email, password, tenant_id, user_context)) def sign_up( - tenant_id: Optional[str], email: str, password: str, + tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None, ): from supertokens_python.recipe.emailpassword.asyncio import sign_up - return sync(sign_up(tenant_id, email, password, user_context)) + return sync(sign_up(email, password, tenant_id, user_context)) def send_email( diff --git a/supertokens_python/recipe/multitenancy/interfaces.py b/supertokens_python/recipe/multitenancy/interfaces.py index ff6579eb8..aa3f692e7 100644 --- a/supertokens_python/recipe/multitenancy/interfaces.py +++ b/supertokens_python/recipe/multitenancy/interfaces.py @@ -71,17 +71,29 @@ class EmailPasswordConfig: def __init__(self, enabled: bool): self.enabled = enabled + def to_json(self): + return {"enabled": self.enabled} + class PasswordlessConfig: def __init__(self, enabled: bool): self.enabled = enabled + def to_json(self): + return {"enabled": self.enabled} + class ThirdPartyConfig: def __init__(self, enabled: bool, providers: List[ProviderConfig]): self.enabled = enabled self.providers = providers + def to_json(self): + return { + "enabled": self.enabled, + "providers": [provider.to_json() for provider in self.providers], + } + class TenantConfigResponse: def __init__( @@ -101,10 +113,32 @@ class GetTenantOkResult(TenantConfigResponse): status = "OK" +class ListAllTenantsItem(TenantConfigResponse): + def __init__( + self, + tenant_id: str, + emailpassword: EmailPasswordConfig, + passwordless: PasswordlessConfig, + third_party: ThirdPartyConfig, + core_config: Dict[str, Any], + ): + super().__init__(emailpassword, passwordless, third_party, core_config) + self.tenant_id = tenant_id + + def to_json(self): + return { + "tenantId": self.tenant_id, + "emailpassword": self.emailpassword.to_json(), + "passwordless": self.passwordless.to_json(), + "thirdParty": self.third_party.to_json(), + "coreConfig": self.core_config, + } + + class ListAllTenantsOkResult: status = "OK" - def __init__(self, tenants: List[TenantConfigResponse]): + def __init__(self, tenants: List[ListAllTenantsItem]): self.tenants = tenants diff --git a/supertokens_python/recipe/multitenancy/recipe_implementation.py b/supertokens_python/recipe/multitenancy/recipe_implementation.py index d595b9191..195f96e85 100644 --- a/supertokens_python/recipe/multitenancy/recipe_implementation.py +++ b/supertokens_python/recipe/multitenancy/recipe_implementation.py @@ -36,6 +36,7 @@ ListAllTenantsOkResult, CreateOrUpdateThirdPartyConfigOkResult, DeleteThirdPartyConfigOkResult, + ListAllTenantsItem, ) if TYPE_CHECKING: @@ -183,13 +184,21 @@ async def list_all_tenants( {}, ) - tenant_configs: List[TenantConfigResponse] = [] + tenant_items: List[ListAllTenantsItem] = [] for tenant in response["tenants"]: - tenant_configs.append(parse_tenant_config(tenant)) + config = parse_tenant_config(tenant) + item = ListAllTenantsItem( + tenant["tenantId"], + config.emailpassword, + config.passwordless, + config.third_party, + config.core_config, + ) + tenant_items.append(item) return ListAllTenantsOkResult( - tenants=tenant_configs, + tenants=tenant_items, ) async def create_or_update_third_party_config( diff --git a/supertokens_python/recipe/session/access_token.py b/supertokens_python/recipe/session/access_token.py index 989cb5d42..b1ad650d1 100644 --- a/supertokens_python/recipe/session/access_token.py +++ b/supertokens_python/recipe/session/access_token.py @@ -155,7 +155,9 @@ def validate_access_token_structure(payload: Dict[str, Any], version: int) -> No if version >= 4: if not isinstance(payload.get("tId"), str): - raise Exception("Access token does not contain all the information. Maybe the structure has changed?") + raise Exception( + "Access token does not contain all the information. Maybe the structure has changed?" + ) elif ( not isinstance(payload.get("sessionHandle"), str) diff --git a/supertokens_python/recipe/session/asyncio/__init__.py b/supertokens_python/recipe/session/asyncio/__init__.py index 94a8ca53f..779e6b3ed 100644 --- a/supertokens_python/recipe/session/asyncio/__init__.py +++ b/supertokens_python/recipe/session/asyncio/__init__.py @@ -426,18 +426,21 @@ async def revoke_session( async def revoke_all_sessions_for_user( - user_id: str, tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None + user_id: str, + tenant_id: Optional[str], + user_context: Union[None, Dict[str, Any]] = None, ) -> List[str]: if user_context is None: user_context = {} return await SessionRecipe.get_instance().recipe_implementation.revoke_all_sessions_for_user( - user_id, tenant_id or DEFAULT_TENANT_ID, - tenant_id is None, user_context + user_id, tenant_id or DEFAULT_TENANT_ID, tenant_id is None, user_context ) async def get_all_session_handles_for_user( - user_id: str, tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None + user_id: str, + tenant_id: Optional[str], + user_context: Union[None, Dict[str, Any]] = None, ) -> List[str]: if user_context is None: user_context = {} diff --git a/supertokens_python/recipe/session/interfaces.py b/supertokens_python/recipe/session/interfaces.py index 39f2183d9..9777c1f5f 100644 --- a/supertokens_python/recipe/session/interfaces.py +++ b/supertokens_python/recipe/session/interfaces.py @@ -41,7 +41,13 @@ class SessionObj: - def __init__(self, handle: str, user_id: str, user_data_in_jwt: Dict[str, Any], tenant_id: str): + def __init__( + self, + handle: str, + user_id: str, + user_data_in_jwt: Dict[str, Any], + tenant_id: str, + ): self.handle = handle self.user_id = user_id self.user_data_in_jwt = user_data_in_jwt @@ -76,7 +82,9 @@ def __init__( self.user_id = user_id self.session_data_in_database = session_data_in_database self.expiry = expiry - self.custom_claims_in_access_token_payload = custom_claims_in_access_token_payload + self.custom_claims_in_access_token_payload = ( + custom_claims_in_access_token_payload + ) self.time_created = time_created self.tenant_id = tenant_id @@ -208,13 +216,21 @@ async def revoke_session( @abstractmethod async def revoke_all_sessions_for_user( - self, user_id: str, tenant_id: str, revoke_across_all_tenants: Optional[bool], user_context: Dict[str, Any] + self, + user_id: str, + tenant_id: str, + revoke_across_all_tenants: Optional[bool], + user_context: Dict[str, Any], ) -> List[str]: pass @abstractmethod async def get_all_session_handles_for_user( - self, user_id: str, tenant_id: str, fetch_across_all_tenants: Optional[bool], user_context: Dict[str, Any] + self, + user_id: str, + tenant_id: str, + fetch_across_all_tenants: Optional[bool], + user_context: Dict[str, Any], ) -> List[str]: pass diff --git a/supertokens_python/recipe/session/recipe_implementation.py b/supertokens_python/recipe/session/recipe_implementation.py index db6044f79..b37e631e0 100644 --- a/supertokens_python/recipe/session/recipe_implementation.py +++ b/supertokens_python/recipe/session/recipe_implementation.py @@ -75,7 +75,7 @@ async def create_new_session( disable_anti_csrf is True, access_token_payload, session_data_in_database, - tenant_id + tenant_id, ) log_debug_message("createNewSession: Finished") @@ -97,7 +97,7 @@ async def create_new_session( payload, None, True, - tenant_id + tenant_id, ) return new_session @@ -327,14 +327,26 @@ async def revoke_session( return await session_functions.revoke_session(self, session_handle) async def revoke_all_sessions_for_user( - self, user_id: str, tenant_id: Optional[str], revoke_across_all_tenants: Optional[bool], user_context: Dict[str, Any] + self, + user_id: str, + tenant_id: Optional[str], + revoke_across_all_tenants: Optional[bool], + user_context: Dict[str, Any], ) -> List[str]: - return await session_functions.revoke_all_sessions_for_user(self, user_id, tenant_id, revoke_across_all_tenants) + return await session_functions.revoke_all_sessions_for_user( + self, user_id, tenant_id, revoke_across_all_tenants + ) async def get_all_session_handles_for_user( - self, user_id: str, tenant_id: Optional[str], fetch_across_all_tenants: Optional[bool], user_context: Dict[str, Any] + self, + user_id: str, + tenant_id: Optional[str], + fetch_across_all_tenants: Optional[bool], + user_context: Dict[str, Any], ) -> List[str]: - return await session_functions.get_all_session_handles_for_user(self, user_id, tenant_id, fetch_across_all_tenants) + return await session_functions.get_all_session_handles_for_user( + self, user_id, tenant_id, fetch_across_all_tenants + ) async def revoke_multiple_sessions( self, session_handles: List[str], user_context: Dict[str, Any] @@ -467,6 +479,6 @@ async def regenerate_access_token( response["session"]["handle"], response["session"]["userId"], response["session"]["userDataInJWT"], - response["session"]["tenantId"] + response["session"]["tenantId"], ) return RegenerateAccessTokenOkResult(session, access_token_obj) diff --git a/supertokens_python/recipe/session/session_class.py b/supertokens_python/recipe/session/session_class.py index 76c54f95c..2bad06285 100644 --- a/supertokens_python/recipe/session/session_class.py +++ b/supertokens_python/recipe/session/session_class.py @@ -11,7 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, Dict, List, TypeVar, Union from supertokens_python.recipe.session.exceptions import ( raise_invalid_claims_exception, diff --git a/supertokens_python/recipe/session/session_functions.py b/supertokens_python/recipe/session/session_functions.py index 3a1546cb8..d3fb1cdca 100644 --- a/supertokens_python/recipe/session/session_functions.py +++ b/supertokens_python/recipe/session/session_functions.py @@ -101,7 +101,7 @@ async def create_new_session( disable_anti_csrf: bool, access_token_payload: Union[None, Dict[str, Any]], session_data_in_database: Union[None, Dict[str, Any]], - tenant_id: Optional[str] + tenant_id: Optional[str], ) -> CreateOrRefreshAPIResponse: if session_data_in_database is None: session_data_in_database = {} @@ -263,7 +263,7 @@ async def get_session( access_token_info["userId"], access_token_info["userData"], access_token_info["expiryTime"], - access_token_info["tenantId"] + access_token_info["tenantId"], ) ) @@ -302,7 +302,9 @@ async def get_session( "expiryTime" ] # if the token didn't pass validation, but we got here, it means it was a v2 token that we didn't have the key cached for. ), # This will throw error if others are none and 'expiryTime' key doesn't exist in the payload - response["session"].get("tenantId") or (access_token_info or {}).get("tenantId") or DEFAULT_TENANT_ID, + response["session"].get("tenantId") + or (access_token_info or {}).get("tenantId") + or DEFAULT_TENANT_ID, ), GetSessionAPIResponseAccessToken( response["accessToken"]["token"], @@ -387,32 +389,33 @@ async def refresh_session( async def revoke_all_sessions_for_user( - recipe_implementation: RecipeImplementation, user_id: str, - tenant_id: Optional[str], revoke_across_all_tenants: Optional[bool], + recipe_implementation: RecipeImplementation, + user_id: str, + tenant_id: Optional[str], + revoke_across_all_tenants: Optional[bool], ) -> List[str]: if tenant_id is None: tenant_id = DEFAULT_TENANT_ID response = await recipe_implementation.querier.send_post_request( NormalisedURLPath(f"{tenant_id}/recipe/session/remove"), - { - "userId": user_id, - "revokeAcrossAllTenants": revoke_across_all_tenants - } + {"userId": user_id, "revokeAcrossAllTenants": revoke_across_all_tenants}, ) return response["sessionHandlesRevoked"] async def get_all_session_handles_for_user( - recipe_implementation: RecipeImplementation, user_id: str, - tenant_id: Optional[str], fetch_across_all_tenants: Optional[bool] + recipe_implementation: RecipeImplementation, + user_id: str, + tenant_id: Optional[str], + fetch_across_all_tenants: Optional[bool], ) -> List[str]: if tenant_id is None: tenant_id = DEFAULT_TENANT_ID response = await recipe_implementation.querier.send_get_request( NormalisedURLPath(f"{tenant_id}/recipe/session/user"), - {"userId": user_id, "fetchAcrossAllTenants": fetch_across_all_tenants} + {"userId": user_id, "fetchAcrossAllTenants": fetch_across_all_tenants}, ) return response["sessionHandles"] @@ -431,8 +434,7 @@ async def revoke_multiple_sessions( recipe_implementation: RecipeImplementation, session_handles: List[str] ) -> List[str]: response = await recipe_implementation.querier.send_post_request( - NormalisedURLPath("/recipe/session/remove"), - {"sessionHandles": session_handles} + NormalisedURLPath("/recipe/session/remove"), {"sessionHandles": session_handles} ) return response["sessionHandlesRevoked"] @@ -481,6 +483,6 @@ async def get_session_information( response["expiry"], response["userDataInJWT"], response["timeCreated"], - response["tenantId"] + response["tenantId"], ) return None diff --git a/supertokens_python/recipe/session/syncio/__init__.py b/supertokens_python/recipe/session/syncio/__init__.py index 29251bc37..fc8798217 100644 --- a/supertokens_python/recipe/session/syncio/__init__.py +++ b/supertokens_python/recipe/session/syncio/__init__.py @@ -187,7 +187,9 @@ def revoke_session( def revoke_all_sessions_for_user( - user_id: str, tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None + user_id: str, + tenant_id: Optional[str], + user_context: Union[None, Dict[str, Any]] = None, ) -> List[str]: from supertokens_python.recipe.session.asyncio import ( revoke_all_sessions_for_user as async_revoke_all_sessions_for_user, @@ -197,13 +199,17 @@ def revoke_all_sessions_for_user( def get_all_session_handles_for_user( - user_id: str, tenant_id: Optional[str], user_context: Union[None, Dict[str, Any]] = None + user_id: str, + tenant_id: Optional[str], + user_context: Union[None, Dict[str, Any]] = None, ) -> List[str]: from supertokens_python.recipe.session.asyncio import ( get_all_session_handles_for_user as async_get_all_session_handles_for_user, ) - return sync(async_get_all_session_handles_for_user(user_id, tenant_id, user_context)) + return sync( + async_get_all_session_handles_for_user(user_id, tenant_id, user_context) + ) def revoke_multiple_sessions( diff --git a/supertokens_python/recipe/thirdparty/api/implementation.py b/supertokens_python/recipe/thirdparty/api/implementation.py index a0b3dacd5..26a9a3edb 100644 --- a/supertokens_python/recipe/thirdparty/api/implementation.py +++ b/supertokens_python/recipe/thirdparty/api/implementation.py @@ -134,7 +134,7 @@ async def sign_in_up_post( session = await create_new_session( api_options.request, user.user_id, - tenant_id, + tenant_id=tenant_id, user_context=user_context, ) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 5c051eb1a..a77bb40e2 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -47,6 +47,7 @@ ) from .always_initialised_recipes import DEFAULT_MULTITENANCY_RECIPE +from supertokens_python.recipe.multitenancy.constants import DEFAULT_TENANT_ID if TYPE_CHECKING: from .recipe_module import RecipeModule @@ -254,7 +255,9 @@ def get_all_cors_headers(self) -> List[str]: return list(headers_set) async def get_user_count( # pylint: disable=no-self-use - self, include_recipe_ids: Union[None, List[str]] + self, + include_recipe_ids: Union[None, List[str]], + tenant_id: Optional[str] = None, ) -> int: querier = Querier.get_instance(None) include_recipe_ids_str = None @@ -262,7 +265,11 @@ async def get_user_count( # pylint: disable=no-self-use include_recipe_ids_str = ",".join(include_recipe_ids) response = await querier.send_get_request( - NormalisedURLPath(USER_COUNT), {"includeRecipeIds": include_recipe_ids_str} + NormalisedURLPath(f"/{tenant_id or DEFAULT_TENANT_ID}{USER_COUNT}"), + { + "includeRecipeIds": include_recipe_ids_str, + "includeAllTenants": tenant_id is None, + }, ) return int(response["count"]) @@ -287,6 +294,7 @@ async def get_users( # pylint: disable=no-self-use pagination_token: Union[str, None], include_recipe_ids: Union[None, List[str]], query: Union[Dict[str, str], None] = None, + tenant_id: Optional[str] = None, ) -> UsersResponse: querier = Querier.get_instance(None) params = {"timeJoinedOrder": time_joined_order} @@ -303,7 +311,12 @@ async def get_users( # pylint: disable=no-self-use if query is not None: params = {**params, **query} - response = await querier.send_get_request(NormalisedURLPath(USERS), params) + if tenant_id is None: + tenant_id = DEFAULT_TENANT_ID + + response = await querier.send_get_request( + NormalisedURLPath(f"/{tenant_id}{USERS}"), params + ) next_pagination_token = None if "nextPaginationToken" in response: next_pagination_token = response["nextPaginationToken"] @@ -331,6 +344,7 @@ async def get_users( # pylint: disable=no-self-use email, phone_number, third_party, + user_obj["tenantIds"], ) ) diff --git a/supertokens_python/types.py b/supertokens_python/types.py index ed08effd2..4ece620e3 100644 --- a/supertokens_python/types.py +++ b/supertokens_python/types.py @@ -32,6 +32,7 @@ def __init__( email: Union[str, None], phone_number: Union[str, None], third_party_info: Union[ThirdPartyInfo, None], + tenant_ids: List[str], ): self.recipe_id = recipe_id self.user_id = user_id @@ -39,6 +40,7 @@ def __init__( self.time_joined = time_joined self.third_party_info = third_party_info self.phone_number = phone_number + self.tenant_ids = tenant_ids def to_json(self) -> Dict[str, Any]: return { @@ -51,6 +53,7 @@ def to_json(self) -> Dict[str, Any]: if self.third_party_info is None else self.third_party_info.__dict__, "phoneNumber": self.phone_number, + "tenantIds": self.tenant_ids, }, }