Module supertokens_python.recipe.emailverification
Expand source code
# Copyright (c) 2021, 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, Optional, Union
from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig
from .emaildelivery.services import SMTPService
from .interfaces import TypeGetEmailForUserIdFunction
from .recipe import EmailVerificationClaim, EmailVerificationRecipe
from .types import EmailDeliveryInterface, EmailTemplateVars
from .utils import MODE_TYPE, EmailVerificationOverrideConfig, InputOverrideConfig
if TYPE_CHECKING:
from supertokens_python.supertokens import RecipeInit
def init(
mode: MODE_TYPE,
email_delivery: Union[EmailDeliveryConfig[EmailTemplateVars], None] = None,
get_email_for_recipe_user_id: Optional[TypeGetEmailForUserIdFunction] = None,
override: Union[EmailVerificationOverrideConfig, None] = None,
) -> RecipeInit:
return EmailVerificationRecipe.init(
mode,
email_delivery,
get_email_for_recipe_user_id,
override,
)
__all__ = [
"EmailDeliveryInterface",
"EmailTemplateVars",
"EmailVerificationClaim",
"EmailVerificationOverrideConfig",
"EmailVerificationRecipe",
"InputOverrideConfig", # deprecated, use EmailVerificationOverrideConfig instead
"SMTPService",
"TypeGetEmailForUserIdFunction",
"init",
]
Sub-modules
supertokens_python.recipe.emailverification.apisupertokens_python.recipe.emailverification.asynciosupertokens_python.recipe.emailverification.constantssupertokens_python.recipe.emailverification.emaildeliverysupertokens_python.recipe.emailverification.exceptionssupertokens_python.recipe.emailverification.interfacessupertokens_python.recipe.emailverification.recipesupertokens_python.recipe.emailverification.recipe_implementationsupertokens_python.recipe.emailverification.synciosupertokens_python.recipe.emailverification.typessupertokens_python.recipe.emailverification.utils
Functions
def init(mode: MODE_TYPE, email_delivery: Union[EmailDeliveryConfig[VerificationEmailTemplateVars], None] = None, get_email_for_recipe_user_id: Optional[TypeGetEmailForUserIdFunction] = None, override: Union[BaseOverrideConfig[RecipeInterface, APIInterface], None] = None)
Classes
class EmailVerificationOverrideConfig (**data: Any)-
Base class for input override config with API overrides.
Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.Ancestors
- BaseOverrideConfig
- BaseOverrideConfigWithoutAPI
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
- typing.Generic
Class variables
var model_config-
The type of the None singleton.
class InputOverrideConfig (**data: Any)-
Base class for input override config with API overrides.
Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.Ancestors
- BaseOverrideConfig
- BaseOverrideConfigWithoutAPI
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
- typing.Generic
Inherited members
class EmailDeliveryInterface-
Helper class that provides a standard way to create an ABC using inheritance.
Expand source code
class EmailDeliveryInterface(ABC, Generic[_T]): @abstractmethod async def send_email(self, template_vars: _T, user_context: Dict[str, Any]) -> None: passAncestors
- abc.ABC
- typing.Generic
Subclasses
- BackwardCompatibilityService
- SMTPService
- BackwardCompatibilityService
- SMTPService
- BackwardCompatibilityService
- SMTPService
- BackwardCompatibilityService
- SMTPService
Methods
async def send_email(self, template_vars: _T, user_context: Dict[str, Any]) ‑> None
class EmailVerificationRecipe (recipe_id: str, app_info: AppInfo, ingredients: EmailVerificationIngredients, config: EmailVerificationConfig)-
Helper class that provides a standard way to create an ABC using inheritance.
Expand source code
class EmailVerificationRecipe(RecipeModule): recipe_id = "emailverification" __instance = None email_delivery: EmailDeliveryIngredient[VerificationEmailTemplateVars] def __init__( self, recipe_id: str, app_info: AppInfo, ingredients: EmailVerificationIngredients, config: EmailVerificationConfig, ) -> None: super().__init__(recipe_id, app_info) self.config = validate_and_normalise_user_input( app_info=app_info, config=config, ) recipe_implementation = RecipeImplementation( Querier.get_instance(recipe_id), self.get_email_for_recipe_user_id, ) self.recipe_implementation = self.config.override.functions( recipe_implementation ) api_implementation = APIImplementation() self.api_implementation = self.config.override.apis(api_implementation) email_delivery_ingredient = ingredients.email_delivery if email_delivery_ingredient is None: self.email_delivery = EmailDeliveryIngredient( self.config.get_email_delivery_config() ) else: self.email_delivery = email_delivery_ingredient def is_error_from_this_recipe_based_on_instance(self, err: Exception) -> bool: return isinstance(err, SuperTokensError) and isinstance( err, SuperTokensEmailVerificationError ) def get_apis_handled(self) -> List[APIHandled]: return [ APIHandled( NormalisedURLPath(USER_EMAIL_VERIFY_TOKEN), "post", USER_EMAIL_VERIFY_TOKEN, self.api_implementation.disable_generate_email_verify_token_post, ), APIHandled( NormalisedURLPath(USER_EMAIL_VERIFY), "post", USER_EMAIL_VERIFY, self.api_implementation.disable_email_verify_post, ), APIHandled( NormalisedURLPath(USER_EMAIL_VERIFY), "get", USER_EMAIL_VERIFY, self.api_implementation.disable_is_email_verified_get, ), ] async def handle_api_request( self, request_id: str, tenant_id: str, request: BaseRequest, path: NormalisedURLPath, method: str, response: BaseResponse, user_context: Dict[str, Any], ) -> Union[BaseResponse, None]: api_options = APIOptions( request, response, self.recipe_id, self.config, self.recipe_implementation, self.get_app_info(), self.email_delivery, ) if request_id == USER_EMAIL_VERIFY_TOKEN: return await handle_generate_email_verify_token_api( self.api_implementation, api_options, user_context ) return await handle_email_verify_api( self.api_implementation, tenant_id, api_options, user_context ) async def handle_error( self, request: BaseRequest, err: SuperTokensError, response: BaseResponse, user_context: Dict[str, Any], ) -> BaseResponse: if isinstance(err, EmailVerificationInvalidTokenError): response.set_json_content( {"status": "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"} ) return response response.set_json_content({"status": "EMAIL_ALREADY_VERIFIED_ERROR"}) return response def get_all_cors_headers(self) -> List[str]: return [] @staticmethod def init( mode: MODE_TYPE, email_delivery: Union[EmailDeliveryConfig[EmailTemplateVars], None] = None, get_email_for_recipe_user_id: Optional[TypeGetEmailForUserIdFunction] = None, override: Optional[EmailVerificationOverrideConfig] = None, ): from supertokens_python.plugins import OverrideMap, apply_plugins config = EmailVerificationConfig( mode=mode, email_delivery=email_delivery, get_email_for_recipe_user_id=get_email_for_recipe_user_id, override=override, ) def func( app_info: AppInfo, plugins: List[OverrideMap] ) -> EmailVerificationRecipe: if EmailVerificationRecipe.__instance is None: ingredients = EmailVerificationIngredients(email_delivery=None) EmailVerificationRecipe.__instance = EmailVerificationRecipe( EmailVerificationRecipe.recipe_id, app_info, ingredients, config=apply_plugins( recipe_id=EmailVerificationRecipe.recipe_id, config=config, plugins=plugins, ), ) def callback(): SessionRecipe.get_instance().add_claim_from_other_recipe( EmailVerificationClaim ) if mode == "REQUIRED": SessionRecipe.get_instance().add_claim_validator_from_other_recipe( EmailVerificationClaim.validators.is_verified() ) from supertokens_python.recipe.accountlinking.recipe import ( AccountLinkingRecipe, ) assert EmailVerificationRecipe.__instance is not None AccountLinkingRecipe.get_instance().register_email_verification_recipe( EmailVerificationRecipe.__instance ) PostSTInitCallbacks.add_post_init_callback(callback) return EmailVerificationRecipe.__instance raise_general_exception( "Emailverification recipe has already been initialised. Please check your code for bugs." ) return func @staticmethod def get_instance_or_throw() -> EmailVerificationRecipe: if EmailVerificationRecipe.__instance is not None: return EmailVerificationRecipe.__instance raise_general_exception( "Initialisation not done. Did you forget to call the SuperTokens.init function?" ) @staticmethod def get_instance_optional() -> Optional[EmailVerificationRecipe]: return EmailVerificationRecipe.__instance @staticmethod def reset(): if ("SUPERTOKENS_ENV" not in environ) or ( environ["SUPERTOKENS_ENV"] != "testing" ): raise_general_exception("calling testing function in non testing env") EmailVerificationRecipe.__instance = None async def get_email_for_recipe_user_id( self, user: Optional[User], recipe_user_id: RecipeUserId, user_context: Dict[str, Any], ) -> Union[GetEmailForUserIdOkResult, EmailDoesNotExistError, UnknownUserIdError]: if self.config.get_email_for_recipe_user_id is not None: user_res = await self.config.get_email_for_recipe_user_id( recipe_user_id, user_context ) if not isinstance(user_res, UnknownUserIdError): return user_res if user is None: from supertokens_python.recipe.accountlinking.recipe import ( AccountLinkingRecipe, ) user = await AccountLinkingRecipe.get_instance().recipe_implementation.get_user( recipe_user_id.get_as_string(), user_context ) if user is None: return UnknownUserIdError() for login_method in user.login_methods: if ( login_method.recipe_user_id.get_as_string() == recipe_user_id.get_as_string() ): if login_method.email is not None: return GetEmailForUserIdOkResult(email=login_method.email) else: return EmailDoesNotExistError() return UnknownUserIdError() async def get_primary_user_id_for_recipe_user( self, recipe_user_id: RecipeUserId, user_context: Dict[str, Any] ) -> str: # We extract this into its own function like this cause we want to make sure that # this recipe does not get the email of the user ID from the getUser function. # In fact, there is a test "email verification recipe uses getUser function only in getEmailForRecipeUserId" # which makes sure that this function is only called in 3 places in this recipe: # - this function # - getEmailForRecipeUserId function (above) # - after verification to get the updated user in verifyEmailUsingToken # We want to isolate the result of calling this function as much as possible # so that the consumer of the getUser function does not read the email # from the primaryUser. Hence, this function only returns the string ID # and nothing else from the primaryUser. from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe primary_user = ( await AccountLinkingRecipe.get_instance().recipe_implementation.get_user( recipe_user_id.get_as_string(), user_context ) ) if primary_user is None: # This can come here if the user is using session + email verification # recipe with a user ID that is not known to supertokens. In this case, # we do not allow linking for such users. return recipe_user_id.get_as_string() return primary_user.id async def update_session_if_required_post_email_verification( self, req: BaseRequest, session: Optional[SessionContainer], recipe_user_id_whose_email_got_verified: RecipeUserId, user_context: Dict[str, Any], ) -> Optional[SessionContainer]: primary_user_id = await self.get_primary_user_id_for_recipe_user( recipe_user_id_whose_email_got_verified, user_context ) # if a session exists in the API, then we can update the session # claim related to email verification if session is not None: log_debug_message( "updateSessionIfRequiredPostEmailVerification got session" ) # Due to linking, we will have to correct the current # session's user ID. There are four cases here: # --> (Case 1) User signed up and did email verification and the new account # became a primary user (user ID no change) # --> (Case 2) User signed up and did email verification and the new account got linked # to another primary user (user ID change) # --> (Case 3) This is post login account linking, in which the account that got verified # got linked to the session's account (user ID of account has changed to the session's user ID) # --> (Case 4) This is post login account linking, in which the account that got verified # got linked to ANOTHER primary account (user ID of account has changed to a different user ID != session.getUserId, but # we should ignore this since it will result in the user's session changing.) if ( session.get_recipe_user_id(user_context).get_as_string() == recipe_user_id_whose_email_got_verified.get_as_string() ): log_debug_message( "updateSessionIfRequiredPostEmailVerification the session belongs to the verified user" ) # this means that the session's login method's account is the # one that just got verified and that we are NOT doing post login # account linking. So this is only for (Case 1) and (Case 2) if session.get_user_id() == primary_user_id: log_debug_message( "updateSessionIfRequiredPostEmailVerification the session userId matches the primary user id, so we are only refreshing the claim" ) # if the session's primary user ID is equal to the # primary user ID that the account was linked to, then # this means that the new account became a primary user (Case 1) # We also have the sub cases here that the account that just # got verified was already linked to the session's primary user ID, # but either way, we don't need to change any user ID. # In this case, all we do is to update the emailverification claim try: # EmailVerificationClaim will be based on the recipeUserId # and not the primary user ID. await session.fetch_and_set_claim( EmailVerificationClaim, user_context ) except Exception as err: # This should never happen, since we've just set the status above. if str(err) == "UNKNOWN_USER_ID": raise_unauthorised_exception("Unknown User ID provided") raise err return None else: log_debug_message( "updateSessionIfRequiredPostEmailVerification the session user id doesn't match the primary user id, so we are revoking all sessions and creating a new one" ) # if the session's primary user ID is NOT equal to the # primary user ID that the account that it was linked to, then # this means that the new account got linked to another primary user (Case 2) # In this case, we need to update the session's user ID by creating # a new session # Revoke all session belonging to session.getRecipeUserId() # We do not really need to do this, but we do it anyway.. no harm. await revoke_all_sessions_for_user( recipe_user_id_whose_email_got_verified.get_as_string(), False, None, user_context, ) # create a new session and return that.. return await create_new_session( req, session.get_tenant_id(), session.get_recipe_user_id(user_context), {}, {}, user_context, ) else: log_debug_message( "updateSessionIfRequiredPostEmailVerification the verified user doesn't match the session" ) # this means that the session's login method's account was NOT the # one that just got verified and that we ARE doing post login # account linking. So this is only for (Case 3) and (Case 4) # In both case 3 and case 4, we do not want to change anything in the # current session in terms of user ID or email verification claim (since # both of these refer to the current logged in user and not the newly # linked user's account). return None else: log_debug_message( "updateSessionIfRequiredPostEmailVerification got no session" ) # the session is updated when the is email verification GET API is called # so we don't do anything in this API. return NoneAncestors
- RecipeModule
- abc.ABC
Class variables
var email_delivery : EmailDeliveryIngredient[VerificationEmailTemplateVars]-
The type of the None singleton.
var recipe_id-
The type of the None singleton.
Static methods
def get_instance_optional() ‑> Optional[EmailVerificationRecipe]def get_instance_or_throw() ‑> EmailVerificationRecipedef init(mode: MODE_TYPE, email_delivery: Union[EmailDeliveryConfig[VerificationEmailTemplateVars], None] = None, get_email_for_recipe_user_id: Optional[TypeGetEmailForUserIdFunction] = None, override: Optional[BaseOverrideConfig[RecipeInterface, APIInterface]] = None)def reset()
Methods
def get_all_cors_headers(self) ‑> List[str]def get_apis_handled(self) ‑> List[APIHandled]async def get_email_for_recipe_user_id(self, user: Optional[User], recipe_user_id: RecipeUserId, user_context: Dict[str, Any])async def get_primary_user_id_for_recipe_user(self, recipe_user_id: RecipeUserId, user_context: Dict[str, Any])async def handle_api_request(self, request_id: str, tenant_id: str, request: BaseRequest, path: NormalisedURLPath, method: str, response: BaseResponse, user_context: Dict[str, Any])async def handle_error(self, request: BaseRequest, err: SuperTokensError, response: BaseResponse, user_context: Dict[str, Any])def is_error_from_this_recipe_based_on_instance(self, err: Exception) ‑> boolasync def update_session_if_required_post_email_verification(self, req: BaseRequest, session: Optional[SessionContainer], recipe_user_id_whose_email_got_verified: RecipeUserId, user_context: Dict[str, Any])
Inherited members
class SMTPService (smtp_settings: SMTPSettings, override: Optional[Callable[[SMTPServiceInterface[VerificationEmailTemplateVars]], SMTPServiceInterface[VerificationEmailTemplateVars]]] = None)-
Helper class that provides a standard way to create an ABC using inheritance.
Expand source code
class SMTPService(EmailDeliveryInterface[VerificationEmailTemplateVars]): service_implementation: SMTPServiceInterface[VerificationEmailTemplateVars] def __init__( self, smtp_settings: SMTPSettings, override: Union[Callable[[SMTPOverrideInput], SMTPOverrideInput], None] = None, ) -> None: transporter = Transporter(smtp_settings) oi = ServiceImplementation(transporter) self.service_implementation = oi if override is None else override(oi) async def send_email( self, template_vars: VerificationEmailTemplateVars, user_context: Dict[str, Any], ) -> None: content = await self.service_implementation.get_content( template_vars, user_context ) await self.service_implementation.send_raw_email(content, user_context)Ancestors
- EmailDeliveryInterface
- abc.ABC
- typing.Generic
Class variables
var service_implementation : SMTPServiceInterface[VerificationEmailTemplateVars]-
The type of the None singleton.
Methods
async def send_email(self, template_vars: VerificationEmailTemplateVars, user_context: Dict[str, Any]) ‑> None
class EmailTemplateVars (user: VerificationEmailTemplateVarsUser, email_verify_link: str, tenant_id: str)-
Expand source code
class VerificationEmailTemplateVars: def __init__( self, user: VerificationEmailTemplateVarsUser, email_verify_link: str, tenant_id: str, ) -> None: self.user = user self.email_verify_link = email_verify_link self.tenant_id = tenant_id