Skip to content

Commit 98e0a07

Browse files
Merge pull request #372 from supertokens/fix/multitenancy-recipe
fix: Update multitenancy recipe
2 parents 63949c3 + 6794ffc commit 98e0a07

File tree

17 files changed

+932
-196
lines changed

17 files changed

+932
-196
lines changed

coreDriverInterfaceSupported.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"_comment": "contains a list of core-driver interfaces branch names that this core supports",
33
"versions": [
4-
"2.21"
4+
"3.0"
55
]
6-
}
6+
}

supertokens_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# under the License.
1414
from __future__ import annotations
1515

16-
SUPPORTED_CDI_VERSIONS = ["2.21"]
16+
SUPPORTED_CDI_VERSIONS = ["3.0"]
1717
VERSION = "0.14.7"
1818
TELEMETRY = "/telemetry"
1919
USER_COUNT = "/users/count"

supertokens_python/normalised_url_path.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def equals(self, other: NormalisedURLPath) -> bool:
3939
return self.__value == other.get_as_string_dangerous()
4040

4141
def is_a_recipe_path(self) -> bool:
42-
return self.__value == "/recipe" or self.__value.startswith("/recipe/")
42+
parts = self.__value.split("/")
43+
return parts[1] == "recipe" or parts[2] == "recipe"
4344

4445

4546
def normalise_url_path_or_throw_error(input_str: str) -> str:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
2+
#
3+
# This software is licensed under the Apache License, Version 2.0 (the
4+
# "License") as published by the Apache Software Foundation.
5+
#
6+
# You may not use this file except in compliance with the License. You may
7+
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from typing import Any, Dict, Optional, Union, List
16+
17+
from supertokens_python.recipe.multitenancy.interfaces import (
18+
APIOptions,
19+
LoginMethodsGetOkResult,
20+
LoginMethodEmailPassword,
21+
LoginMethodPasswordless,
22+
LoginMethodThirdParty,
23+
)
24+
from supertokens_python.types import GeneralErrorResponse
25+
26+
from ..interfaces import APIInterface, ThirdPartyProvider
27+
28+
from supertokens_python.recipe.thirdparty.providers.config_utils import (
29+
merge_providers_from_core_and_static,
30+
find_and_create_provider_instance,
31+
)
32+
from supertokens_python.recipe.thirdparty.exceptions import ClientTypeNotFoundError
33+
34+
35+
class APIImplementation(APIInterface):
36+
async def login_methods_get(
37+
self,
38+
tenant_id: Optional[str],
39+
client_type: Optional[str],
40+
api_options: APIOptions,
41+
user_context: Dict[str, Any],
42+
) -> Union[LoginMethodsGetOkResult, GeneralErrorResponse]:
43+
tenant_config_res = await api_options.recipe_implementation.get_tenant(
44+
tenant_id, user_context
45+
)
46+
47+
provider_inputs_from_static = api_options.static_third_party_providers
48+
provider_configs_from_core = tenant_config_res.third_party.providers
49+
50+
merged_providers = merge_providers_from_core_and_static(
51+
provider_configs_from_core, provider_inputs_from_static
52+
)
53+
54+
final_provider_list: List[ThirdPartyProvider] = []
55+
56+
for provider_input in merged_providers:
57+
try:
58+
provider_instance = await find_and_create_provider_instance(
59+
merged_providers,
60+
provider_input.config.third_party_id,
61+
client_type,
62+
user_context,
63+
)
64+
final_provider_list.append(
65+
ThirdPartyProvider(
66+
provider_instance.id, provider_instance.config.name
67+
)
68+
)
69+
except Exception as e:
70+
if isinstance(e, ClientTypeNotFoundError):
71+
continue
72+
raise e
73+
74+
return LoginMethodsGetOkResult(
75+
email_password=LoginMethodEmailPassword(
76+
tenant_config_res.emailpassword.enabled
77+
),
78+
passwordless=LoginMethodPasswordless(
79+
tenant_config_res.passwordless.enabled
80+
),
81+
third_party=LoginMethodThirdParty(
82+
tenant_config_res.third_party.enabled, final_provider_list
83+
),
84+
)

supertokens_python/recipe/multitenancy/asyncio/__init__.py

Lines changed: 100 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,131 +10,138 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13-
from typing import Any, Dict, Union, Optional
14-
15-
from supertokens_python.recipe.emailverification.interfaces import (
16-
GetEmailForUserIdOkResult,
17-
EmailDoesNotExistError,
18-
CreateEmailVerificationTokenEmailAlreadyVerifiedError,
19-
UnverifyEmailOkResult,
20-
CreateEmailVerificationTokenOkResult,
21-
RevokeEmailVerificationTokensOkResult,
13+
from __future__ import annotations
14+
15+
from typing import Any, Dict, Union, Optional, TYPE_CHECKING
16+
17+
from ..interfaces import (
18+
TenantConfig,
19+
CreateOrUpdateTenantOkResult,
20+
DeleteTenantOkResult,
21+
GetTenantOkResult,
22+
ListAllTenantsOkResult,
23+
CreateOrUpdateThirdPartyConfigOkResult,
24+
DeleteThirdPartyConfigOkResult,
25+
AssociateUserToTenantOkResult,
26+
AssociateUserToTenantUnknownUserIdError,
27+
AssociateUserToTenantEmailAlreadyExistsError,
28+
AssociateUserToTenantPhoneNumberAlreadyExistsError,
29+
AssociateUserToTenantThirdPartyUserAlreadyExistsError,
30+
DisassociateUserFromTenantOkResult,
2231
)
23-
from supertokens_python.recipe.emailverification.types import EmailTemplateVars
24-
from supertokens_python.recipe.emailverification.recipe import EmailVerificationRecipe
32+
from ..recipe import MultitenancyRecipe
2533

34+
if TYPE_CHECKING:
35+
from ..interfaces import ProviderConfig
2636

27-
async def create_email_verification_token(
28-
user_id: str,
29-
email: Optional[str] = None,
30-
user_context: Union[None, Dict[str, Any]] = None,
31-
) -> Union[
32-
CreateEmailVerificationTokenOkResult,
33-
CreateEmailVerificationTokenEmailAlreadyVerifiedError,
34-
]:
37+
38+
async def create_or_update_tenant(
39+
tenant_id: Optional[str],
40+
config: TenantConfig,
41+
user_context: Optional[Dict[str, Any]] = None,
42+
) -> CreateOrUpdateTenantOkResult:
3543
if user_context is None:
3644
user_context = {}
37-
recipe = EmailVerificationRecipe.get_instance()
38-
if email is None:
39-
email_info = await recipe.get_email_for_user_id(user_id, user_context)
40-
if isinstance(email_info, GetEmailForUserIdOkResult):
41-
email = email_info.email
42-
elif isinstance(email_info, EmailDoesNotExistError):
43-
return CreateEmailVerificationTokenEmailAlreadyVerifiedError()
44-
else:
45-
raise Exception("Unknown User ID provided without email")
46-
47-
return await recipe.recipe_implementation.create_email_verification_token(
48-
user_id, email, user_context
45+
recipe = MultitenancyRecipe.get_instance()
46+
47+
return await recipe.recipe_implementation.create_or_update_tenant(
48+
tenant_id, config, user_context
4949
)
5050

5151

52-
async def verify_email_using_token(
53-
token: str, user_context: Union[None, Dict[str, Any]] = None
54-
):
52+
async def delete_tenant(
53+
tenant_id: str, user_context: Optional[Dict[str, Any]] = None
54+
) -> DeleteTenantOkResult:
5555
if user_context is None:
5656
user_context = {}
57-
return await EmailVerificationRecipe.get_instance().recipe_implementation.verify_email_using_token(
58-
token, user_context
59-
)
57+
recipe = MultitenancyRecipe.get_instance()
6058

59+
return await recipe.recipe_implementation.delete_tenant(tenant_id, user_context)
6160

62-
async def is_email_verified(
63-
user_id: str,
64-
email: Optional[str] = None,
65-
user_context: Union[None, Dict[str, Any]] = None,
66-
):
61+
62+
async def get_tenant(
63+
tenant_id: Optional[str], user_context: Optional[Dict[str, Any]] = None
64+
) -> GetTenantOkResult:
65+
if user_context is None:
66+
user_context = {}
67+
recipe = MultitenancyRecipe.get_instance()
68+
69+
return await recipe.recipe_implementation.get_tenant(tenant_id, user_context)
70+
71+
72+
async def list_all_tenants(
73+
user_context: Optional[Dict[str, Any]] = None
74+
) -> ListAllTenantsOkResult:
75+
if user_context is None:
76+
user_context = {}
77+
78+
recipe = MultitenancyRecipe.get_instance()
79+
80+
return await recipe.recipe_implementation.list_all_tenants(user_context)
81+
82+
83+
async def create_or_update_third_party_config(
84+
tenant_id: Optional[str],
85+
config: ProviderConfig,
86+
skip_validation: Optional[bool] = None,
87+
user_context: Optional[Dict[str, Any]] = None,
88+
) -> CreateOrUpdateThirdPartyConfigOkResult:
6789
if user_context is None:
6890
user_context = {}
6991

70-
recipe = EmailVerificationRecipe.get_instance()
71-
if email is None:
72-
email_info = await recipe.get_email_for_user_id(user_id, user_context)
73-
if isinstance(email_info, GetEmailForUserIdOkResult):
74-
email = email_info.email
75-
elif isinstance(email_info, EmailDoesNotExistError):
76-
return True
77-
else:
78-
raise Exception("Unknown User ID provided without email")
79-
80-
return await recipe.recipe_implementation.is_email_verified(
81-
user_id, email, user_context
92+
recipe = MultitenancyRecipe.get_instance()
93+
94+
return await recipe.recipe_implementation.create_or_update_third_party_config(
95+
tenant_id, config, skip_validation, user_context
8296
)
8397

8498

85-
async def revoke_email_verification_tokens(
86-
user_id: str,
87-
email: Optional[str] = None,
99+
async def delete_third_party_config(
100+
tenant_id: Optional[str],
101+
third_party_id: str,
88102
user_context: Optional[Dict[str, Any]] = None,
89-
) -> RevokeEmailVerificationTokensOkResult:
103+
) -> DeleteThirdPartyConfigOkResult:
90104
if user_context is None:
91105
user_context = {}
92106

93-
recipe = EmailVerificationRecipe.get_instance()
94-
if email is None:
95-
email_info = await recipe.get_email_for_user_id(user_id, user_context)
96-
if isinstance(email_info, GetEmailForUserIdOkResult):
97-
email = email_info.email
98-
elif isinstance(email_info, EmailDoesNotExistError):
99-
return RevokeEmailVerificationTokensOkResult()
100-
else:
101-
raise Exception("Unknown User ID provided without email")
102-
103-
return await EmailVerificationRecipe.get_instance().recipe_implementation.revoke_email_verification_tokens(
104-
user_id, email, user_context
107+
recipe = MultitenancyRecipe.get_instance()
108+
109+
return await recipe.recipe_implementation.delete_third_party_config(
110+
tenant_id, third_party_id, user_context
105111
)
106112

107113

108-
async def unverify_email(
114+
async def associate_user_to_tenant(
115+
tenant_id: Optional[str],
109116
user_id: str,
110-
email: Optional[str] = None,
111-
user_context: Union[None, Dict[str, Any]] = None,
112-
):
117+
user_context: Optional[Dict[str, Any]] = None,
118+
) -> Union[
119+
AssociateUserToTenantOkResult,
120+
AssociateUserToTenantUnknownUserIdError,
121+
AssociateUserToTenantEmailAlreadyExistsError,
122+
AssociateUserToTenantPhoneNumberAlreadyExistsError,
123+
AssociateUserToTenantThirdPartyUserAlreadyExistsError,
124+
]:
113125
if user_context is None:
114126
user_context = {}
115127

116-
recipe = EmailVerificationRecipe.get_instance()
117-
if email is None:
118-
email_info = await recipe.get_email_for_user_id(user_id, user_context)
119-
if isinstance(email_info, GetEmailForUserIdOkResult):
120-
email = email_info.email
121-
elif isinstance(email_info, EmailDoesNotExistError):
122-
# Here we are returning OK since that's how it used to work, but a later call
123-
# to is_verified will still return true
124-
return UnverifyEmailOkResult
125-
else:
126-
raise Exception("Unknown User ID provided without email")
127-
128-
return await EmailVerificationRecipe.get_instance().recipe_implementation.unverify_email(
129-
user_id, email, user_context
128+
recipe = MultitenancyRecipe.get_instance()
129+
130+
return await recipe.recipe_implementation.associate_user_to_tenant(
131+
tenant_id, user_id, user_context
130132
)
131133

132134

133-
async def send_email(
134-
input_: EmailTemplateVars, user_context: Union[None, Dict[str, Any]] = None
135-
):
135+
async def dissociate_user_from_tenant(
136+
tenant_id: Optional[str],
137+
user_id: str,
138+
user_context: Optional[Dict[str, Any]] = None,
139+
) -> DisassociateUserFromTenantOkResult:
136140
if user_context is None:
137141
user_context = {}
138-
return await EmailVerificationRecipe.get_instance().email_delivery.ingredient_interface_impl.send_email(
139-
input_, user_context
142+
143+
recipe = MultitenancyRecipe.get_instance()
144+
145+
return await recipe.recipe_implementation.dissociate_user_from_tenant(
146+
tenant_id, user_id, user_context
140147
)

0 commit comments

Comments
 (0)