Skip to content

fix: Update multitenancy recipe #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions coreDriverInterfaceSupported.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "contains a list of core-driver interfaces branch names that this core supports",
"versions": [
"2.21"
"3.0"
]
}
}
2 changes: 1 addition & 1 deletion supertokens_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# under the License.
from __future__ import annotations

SUPPORTED_CDI_VERSIONS = ["2.21"]
SUPPORTED_CDI_VERSIONS = ["3.0"]
VERSION = "0.14.7"
TELEMETRY = "/telemetry"
USER_COUNT = "/users/count"
Expand Down
3 changes: 2 additions & 1 deletion supertokens_python/normalised_url_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def equals(self, other: NormalisedURLPath) -> bool:
return self.__value == other.get_as_string_dangerous()

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


def normalise_url_path_or_throw_error(input_str: str) -> str:
Expand Down
84 changes: 84 additions & 0 deletions supertokens_python/recipe/multitenancy/api/implementation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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 typing import Any, Dict, Optional, Union, List

from supertokens_python.recipe.multitenancy.interfaces import (
APIOptions,
LoginMethodsGetOkResult,
LoginMethodEmailPassword,
LoginMethodPasswordless,
LoginMethodThirdParty,
)
from supertokens_python.types import GeneralErrorResponse

from ..interfaces import APIInterface, ThirdPartyProvider

from supertokens_python.recipe.thirdparty.providers.config_utils import (
merge_providers_from_core_and_static,
find_and_create_provider_instance,
)
from supertokens_python.recipe.thirdparty.exceptions import ClientTypeNotFoundError


class APIImplementation(APIInterface):
async def login_methods_get(
self,
tenant_id: Optional[str],
client_type: Optional[str],
api_options: APIOptions,
user_context: Dict[str, Any],
) -> Union[LoginMethodsGetOkResult, GeneralErrorResponse]:
tenant_config_res = await api_options.recipe_implementation.get_tenant(
tenant_id, user_context
)

provider_inputs_from_static = api_options.static_third_party_providers
provider_configs_from_core = tenant_config_res.third_party.providers

merged_providers = merge_providers_from_core_and_static(
provider_configs_from_core, provider_inputs_from_static
)

final_provider_list: List[ThirdPartyProvider] = []

for provider_input in merged_providers:
try:
provider_instance = await find_and_create_provider_instance(
merged_providers,
provider_input.config.third_party_id,
client_type,
user_context,
)
final_provider_list.append(
ThirdPartyProvider(
provider_instance.id, provider_instance.config.name
)
)
except Exception as e:
if isinstance(e, ClientTypeNotFoundError):
continue
raise e

return LoginMethodsGetOkResult(
email_password=LoginMethodEmailPassword(
tenant_config_res.emailpassword.enabled
),
passwordless=LoginMethodPasswordless(
tenant_config_res.passwordless.enabled
),
third_party=LoginMethodThirdParty(
tenant_config_res.third_party.enabled, final_provider_list
),
)
193 changes: 100 additions & 93 deletions supertokens_python/recipe/multitenancy/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,131 +10,138 @@
# 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, Union, Optional

from supertokens_python.recipe.emailverification.interfaces import (
GetEmailForUserIdOkResult,
EmailDoesNotExistError,
CreateEmailVerificationTokenEmailAlreadyVerifiedError,
UnverifyEmailOkResult,
CreateEmailVerificationTokenOkResult,
RevokeEmailVerificationTokensOkResult,
from __future__ import annotations

from typing import Any, Dict, Union, Optional, TYPE_CHECKING

from ..interfaces import (
TenantConfig,
CreateOrUpdateTenantOkResult,
DeleteTenantOkResult,
GetTenantOkResult,
ListAllTenantsOkResult,
CreateOrUpdateThirdPartyConfigOkResult,
DeleteThirdPartyConfigOkResult,
AssociateUserToTenantOkResult,
AssociateUserToTenantUnknownUserIdError,
AssociateUserToTenantEmailAlreadyExistsError,
AssociateUserToTenantPhoneNumberAlreadyExistsError,
AssociateUserToTenantThirdPartyUserAlreadyExistsError,
DisassociateUserFromTenantOkResult,
)
from supertokens_python.recipe.emailverification.types import EmailTemplateVars
from supertokens_python.recipe.emailverification.recipe import EmailVerificationRecipe
from ..recipe import MultitenancyRecipe

if TYPE_CHECKING:
from ..interfaces import ProviderConfig

async def create_email_verification_token(
user_id: str,
email: Optional[str] = None,
user_context: Union[None, Dict[str, Any]] = None,
) -> Union[
CreateEmailVerificationTokenOkResult,
CreateEmailVerificationTokenEmailAlreadyVerifiedError,
]:

async def create_or_update_tenant(
tenant_id: Optional[str],
config: TenantConfig,
user_context: Optional[Dict[str, Any]] = None,
) -> CreateOrUpdateTenantOkResult:
if user_context is None:
user_context = {}
recipe = EmailVerificationRecipe.get_instance()
if email is None:
email_info = await recipe.get_email_for_user_id(user_id, user_context)
if isinstance(email_info, GetEmailForUserIdOkResult):
email = email_info.email
elif isinstance(email_info, EmailDoesNotExistError):
return CreateEmailVerificationTokenEmailAlreadyVerifiedError()
else:
raise Exception("Unknown User ID provided without email")

return await recipe.recipe_implementation.create_email_verification_token(
user_id, email, user_context
recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.create_or_update_tenant(
tenant_id, config, user_context
)


async def verify_email_using_token(
token: str, user_context: Union[None, Dict[str, Any]] = None
):
async def delete_tenant(
tenant_id: str, user_context: Optional[Dict[str, Any]] = None
) -> DeleteTenantOkResult:
if user_context is None:
user_context = {}
return await EmailVerificationRecipe.get_instance().recipe_implementation.verify_email_using_token(
token, user_context
)
recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.delete_tenant(tenant_id, user_context)

async def is_email_verified(
user_id: str,
email: Optional[str] = None,
user_context: Union[None, Dict[str, Any]] = None,
):

async def get_tenant(
tenant_id: Optional[str], user_context: Optional[Dict[str, Any]] = None
) -> GetTenantOkResult:
if user_context is None:
user_context = {}
recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.get_tenant(tenant_id, user_context)


async def list_all_tenants(
user_context: Optional[Dict[str, Any]] = None
) -> ListAllTenantsOkResult:
if user_context is None:
user_context = {}

recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.list_all_tenants(user_context)


async def create_or_update_third_party_config(
tenant_id: Optional[str],
config: ProviderConfig,
skip_validation: Optional[bool] = None,
user_context: Optional[Dict[str, Any]] = None,
) -> CreateOrUpdateThirdPartyConfigOkResult:
if user_context is None:
user_context = {}

recipe = EmailVerificationRecipe.get_instance()
if email is None:
email_info = await recipe.get_email_for_user_id(user_id, user_context)
if isinstance(email_info, GetEmailForUserIdOkResult):
email = email_info.email
elif isinstance(email_info, EmailDoesNotExistError):
return True
else:
raise Exception("Unknown User ID provided without email")

return await recipe.recipe_implementation.is_email_verified(
user_id, email, user_context
recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.create_or_update_third_party_config(
tenant_id, config, skip_validation, user_context
)


async def revoke_email_verification_tokens(
user_id: str,
email: Optional[str] = None,
async def delete_third_party_config(
tenant_id: Optional[str],
third_party_id: str,
user_context: Optional[Dict[str, Any]] = None,
) -> RevokeEmailVerificationTokensOkResult:
) -> DeleteThirdPartyConfigOkResult:
if user_context is None:
user_context = {}

recipe = EmailVerificationRecipe.get_instance()
if email is None:
email_info = await recipe.get_email_for_user_id(user_id, user_context)
if isinstance(email_info, GetEmailForUserIdOkResult):
email = email_info.email
elif isinstance(email_info, EmailDoesNotExistError):
return RevokeEmailVerificationTokensOkResult()
else:
raise Exception("Unknown User ID provided without email")

return await EmailVerificationRecipe.get_instance().recipe_implementation.revoke_email_verification_tokens(
user_id, email, user_context
recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.delete_third_party_config(
tenant_id, third_party_id, user_context
)


async def unverify_email(
async def associate_user_to_tenant(
tenant_id: Optional[str],
user_id: str,
email: Optional[str] = None,
user_context: Union[None, Dict[str, Any]] = None,
):
user_context: Optional[Dict[str, Any]] = None,
) -> Union[
AssociateUserToTenantOkResult,
AssociateUserToTenantUnknownUserIdError,
AssociateUserToTenantEmailAlreadyExistsError,
AssociateUserToTenantPhoneNumberAlreadyExistsError,
AssociateUserToTenantThirdPartyUserAlreadyExistsError,
]:
if user_context is None:
user_context = {}

recipe = EmailVerificationRecipe.get_instance()
if email is None:
email_info = await recipe.get_email_for_user_id(user_id, user_context)
if isinstance(email_info, GetEmailForUserIdOkResult):
email = email_info.email
elif isinstance(email_info, EmailDoesNotExistError):
# Here we are returning OK since that's how it used to work, but a later call
# to is_verified will still return true
return UnverifyEmailOkResult
else:
raise Exception("Unknown User ID provided without email")

return await EmailVerificationRecipe.get_instance().recipe_implementation.unverify_email(
user_id, email, user_context
recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.associate_user_to_tenant(
tenant_id, user_id, user_context
)


async def send_email(
input_: EmailTemplateVars, user_context: Union[None, Dict[str, Any]] = None
):
async def dissociate_user_from_tenant(
tenant_id: Optional[str],
user_id: str,
user_context: Optional[Dict[str, Any]] = None,
) -> DisassociateUserFromTenantOkResult:
if user_context is None:
user_context = {}
return await EmailVerificationRecipe.get_instance().email_delivery.ingredient_interface_impl.send_email(
input_, user_context

recipe = MultitenancyRecipe.get_instance()

return await recipe.recipe_implementation.dissociate_user_from_tenant(
tenant_id, user_id, user_context
)
Loading