Module supertokens_python.recipe.emailpassword.utils
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 re import fullmatch
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
from supertokens_python.framework import BaseRequest
from supertokens_python.ingredients.emaildelivery.types import (
EmailDeliveryConfig,
EmailDeliveryConfigWithService,
)
from supertokens_python.recipe.emailpassword.emaildelivery.services.backward_compatibility import (
BackwardCompatibilityService,
)
from supertokens_python.types.config import (
BaseConfig,
BaseNormalisedConfig,
BaseNormalisedOverrideConfig,
BaseOverrideableConfig,
BaseOverrideConfig,
)
from supertokens_python.utils import get_filtered_list
from .constants import FORM_FIELD_EMAIL_ID, FORM_FIELD_PASSWORD_ID
from .interfaces import APIInterface, RecipeInterface
from .types import EmailTemplateVars, InputFormField, NormalisedFormField
if TYPE_CHECKING:
from supertokens_python.supertokens import AppInfo
async def default_validator(_: str, __: str) -> Union[str, None]:
return None
async def default_password_validator(value: str, _tenant_id: str) -> Union[str, None]:
# length >= 8 && < 100
# must have a number and a character
# as per
# https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438
if len(value) < 8:
return "Password must contain at least 8 characters, including a number"
if len(value) >= 100:
return "Password's length must be lesser than 100 characters"
if fullmatch(r"^.*[A-Za-z]+.*$", value) is None:
return "Password must contain at least one alphabet"
if fullmatch(r"^.*[0-9]+.*$", value) is None:
return "Password must contain at least one number"
return None
async def default_email_validator(value: Any, _tenant_id: str) -> Union[str, None]:
# We check if the email syntax is correct
# As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438
# Regex from https://stackoverflow.com/a/46181/3867175
if (not isinstance(value, str)) or (
fullmatch(
r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,'
r"3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$",
value,
)
is None
):
return "Email is not valid"
return None
class InputSignUpFeature:
def __init__(self, form_fields: Union[List[InputFormField], None] = None):
if form_fields is None:
form_fields = []
self.form_fields = normalise_sign_up_form_fields(form_fields)
class SignUpFeature:
def __init__(self, form_fields: List[NormalisedFormField]):
self.form_fields = form_fields
def normalise_sign_up_form_fields(
form_fields: List[InputFormField],
) -> List[NormalisedFormField]:
normalised_form_fields: List[NormalisedFormField] = []
for field in form_fields:
if field.id == FORM_FIELD_PASSWORD_ID:
validator = (
field.validate
if field.validate is not None
else default_password_validator
)
normalised_form_fields.append(
NormalisedFormField(field.id, validator, False)
)
elif field.id == FORM_FIELD_EMAIL_ID:
validator = (
field.validate
if field.validate is not None
else default_email_validator
)
normalised_form_fields.append(
NormalisedFormField(field.id, validator, False)
)
else:
validator = (
field.validate if field.validate is not None else default_validator
)
optional = field.optional if field.optional is not None else False
normalised_form_fields.append(
NormalisedFormField(field.id, validator, optional)
)
if (
len(
get_filtered_list(
lambda x: x.id == FORM_FIELD_PASSWORD_ID, normalised_form_fields
)
)
== 0
):
normalised_form_fields.append(
NormalisedFormField(
FORM_FIELD_PASSWORD_ID, default_password_validator, False
)
)
if (
len(
get_filtered_list(
lambda x: x.id == FORM_FIELD_EMAIL_ID, normalised_form_fields
)
)
== 0
):
normalised_form_fields.append(
NormalisedFormField(FORM_FIELD_EMAIL_ID, default_email_validator, False)
)
return normalised_form_fields
class SignInFeature:
def __init__(self, form_fields: List[NormalisedFormField]):
self.form_fields = form_fields
def normalise_sign_in_form_fields(
form_fields: List[NormalisedFormField],
) -> List[NormalisedFormField]:
return list(
map(
lambda y: NormalisedFormField(
y.id,
y.validate if y.id == FORM_FIELD_EMAIL_ID else default_validator,
False,
),
get_filtered_list(
lambda x: x.id in (FORM_FIELD_PASSWORD_ID, FORM_FIELD_EMAIL_ID),
form_fields,
),
)
)
def validate_and_normalise_sign_in_config(
sign_up_config: SignUpFeature,
) -> SignInFeature:
form_fields = normalise_sign_in_form_fields(sign_up_config.form_fields)
return SignInFeature(form_fields)
class ResetPasswordUsingTokenFeature:
def __init__(
self,
form_fields_for_password_reset_form: List[NormalisedFormField],
form_fields_for_generate_token_form: List[NormalisedFormField],
):
self.form_fields_for_password_reset_form = form_fields_for_password_reset_form
self.form_fields_for_generate_token_form = form_fields_for_generate_token_form
def validate_and_normalise_reset_password_using_token_config(
sign_up_config: InputSignUpFeature,
) -> ResetPasswordUsingTokenFeature:
form_fields_for_password_reset_form = list(
map(
lambda y: NormalisedFormField(y.id, y.validate, False),
get_filtered_list(
lambda x: x.id == FORM_FIELD_PASSWORD_ID, sign_up_config.form_fields
),
)
)
form_fields_for_generate_token_form = list(
map(
lambda y: NormalisedFormField(y.id, y.validate, False),
get_filtered_list(
lambda x: x.id == FORM_FIELD_EMAIL_ID, sign_up_config.form_fields
),
)
)
return ResetPasswordUsingTokenFeature(
form_fields_for_password_reset_form,
form_fields_for_generate_token_form,
)
EmailPasswordOverrideConfig = BaseOverrideConfig[RecipeInterface, APIInterface]
NormalisedEmailPasswordOverrideConfig = BaseNormalisedOverrideConfig[
RecipeInterface, APIInterface
]
InputOverrideConfig = EmailPasswordOverrideConfig
"""Deprecated, use `EmailPasswordOverrideConfig` instead."""
class EmailPasswordOverrideableConfig(BaseOverrideableConfig):
"""Input config properties overrideable using the plugin config overrides"""
sign_up_feature: Union[InputSignUpFeature, None] = None
email_delivery: Union[EmailDeliveryConfig[EmailTemplateVars], None] = None
class EmailPasswordConfig(
EmailPasswordOverrideableConfig,
BaseConfig[RecipeInterface, APIInterface, EmailPasswordOverrideableConfig],
):
def to_overrideable_config(self) -> EmailPasswordOverrideableConfig:
"""Create a `EmailPasswordOverrideableConfig` from the current config."""
return EmailPasswordOverrideableConfig(**self.model_dump())
def from_overrideable_config(
self,
overrideable_config: EmailPasswordOverrideableConfig,
) -> "EmailPasswordConfig":
"""
Create a `EmailPasswordConfig` from a `EmailPasswordOverrideableConfig`.
Not a classmethod since it needs to be used in a dynamic context within plugins.
"""
return EmailPasswordConfig(
**overrideable_config.model_dump(),
override=self.override,
)
class NormalisedEmailPasswordConfig(
BaseNormalisedConfig[RecipeInterface, APIInterface]
):
sign_up_feature: SignUpFeature
sign_in_feature: SignInFeature
reset_password_using_token_feature: ResetPasswordUsingTokenFeature
get_email_delivery_config: Callable[
[RecipeInterface], EmailDeliveryConfigWithService[EmailTemplateVars]
]
def validate_and_normalise_user_input(
app_info: AppInfo,
config: EmailPasswordConfig,
) -> NormalisedEmailPasswordConfig:
# NOTE: We don't need to check the instance of sign_up_feature and override
# as they will always be either None or the specified type.
override_config = NormalisedEmailPasswordOverrideConfig.from_input_config(
override_config=config.override
)
sign_up_feature = config.sign_up_feature
if sign_up_feature is None:
sign_up_feature = InputSignUpFeature()
def get_email_delivery_config(
ep_recipe: RecipeInterface,
) -> EmailDeliveryConfigWithService[EmailTemplateVars]:
if config.email_delivery and config.email_delivery.service:
return EmailDeliveryConfigWithService(
service=config.email_delivery.service,
override=config.email_delivery.override,
)
email_service = BackwardCompatibilityService(
app_info=app_info,
recipe_interface_impl=ep_recipe,
)
if (
config.email_delivery is not None
and config.email_delivery.override is not None
):
override = config.email_delivery.override
else:
override = None
return EmailDeliveryConfigWithService(email_service, override=override)
return NormalisedEmailPasswordConfig(
sign_up_feature=SignUpFeature(sign_up_feature.form_fields),
sign_in_feature=SignInFeature(
normalise_sign_in_form_fields(sign_up_feature.form_fields)
),
reset_password_using_token_feature=validate_and_normalise_reset_password_using_token_config(
sign_up_feature
),
override=override_config,
get_email_delivery_config=get_email_delivery_config,
)
def get_password_reset_link(
app_info: AppInfo,
token: str,
tenant_id: str,
request: Optional[BaseRequest],
user_context: Dict[str, Any],
) -> str:
return (
app_info.get_origin(request, user_context).get_as_string_dangerous()
+ app_info.website_base_path.get_as_string_dangerous()
+ "/reset-password?token="
+ token
+ "&tenantId="
+ tenant_id
)
Functions
async def default_email_validator(value: Any, _tenant_id: str) ‑> Optional[str]async def default_password_validator(value: str, _tenant_id: str) ‑> Optional[str]async def default_validator(_: str, __: str) ‑> Optional[str]def get_password_reset_link(app_info: AppInfo, token: str, tenant_id: str, request: Optional[BaseRequest], user_context: Dict[str, Any])def normalise_sign_in_form_fields(form_fields: List[NormalisedFormField]) ‑> List[NormalisedFormField]def normalise_sign_up_form_fields(form_fields: List[InputFormField]) ‑> List[NormalisedFormField]def validate_and_normalise_reset_password_using_token_config(sign_up_config: InputSignUpFeature) ‑> ResetPasswordUsingTokenFeaturedef validate_and_normalise_sign_in_config(sign_up_config: SignUpFeature) ‑> SignInFeaturedef validate_and_normalise_user_input(app_info: AppInfo, config: EmailPasswordConfig)
Classes
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 EmailPasswordConfig (**data: Any)-
Input config properties overrideable using the plugin config 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.Expand source code
class EmailPasswordConfig( EmailPasswordOverrideableConfig, BaseConfig[RecipeInterface, APIInterface, EmailPasswordOverrideableConfig], ): def to_overrideable_config(self) -> EmailPasswordOverrideableConfig: """Create a `EmailPasswordOverrideableConfig` from the current config.""" return EmailPasswordOverrideableConfig(**self.model_dump()) def from_overrideable_config( self, overrideable_config: EmailPasswordOverrideableConfig, ) -> "EmailPasswordConfig": """ Create a `EmailPasswordConfig` from a `EmailPasswordOverrideableConfig`. Not a classmethod since it needs to be used in a dynamic context within plugins. """ return EmailPasswordConfig( **overrideable_config.model_dump(), override=self.override, )Ancestors
- EmailPasswordOverrideableConfig
- BaseOverrideableConfig
- supertokens_python.types.config.BaseConfig[RecipeInterface, APIInterface, EmailPasswordOverrideableConfig]
- BaseConfig
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
- typing.Generic
Methods
def from_overrideable_config(self, overrideable_config: EmailPasswordOverrideableConfig) ‑> EmailPasswordConfig-
Create a
EmailPasswordConfigfrom aEmailPasswordOverrideableConfig. Not a classmethod since it needs to be used in a dynamic context within plugins. def to_overrideable_config(self) ‑> EmailPasswordOverrideableConfig-
Create a
EmailPasswordOverrideableConfigfrom the current config.
Inherited members
class EmailPasswordOverrideableConfig (**data: Any)-
Input config properties overrideable using the plugin config 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.Expand source code
class EmailPasswordOverrideableConfig(BaseOverrideableConfig): """Input config properties overrideable using the plugin config overrides""" sign_up_feature: Union[InputSignUpFeature, None] = None email_delivery: Union[EmailDeliveryConfig[EmailTemplateVars], None] = NoneAncestors
- BaseOverrideableConfig
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
Subclasses
Class variables
var email_delivery : Optional[EmailDeliveryConfig[PasswordResetEmailTemplateVars]]-
The type of the None singleton.
var sign_up_feature : Optional[InputSignUpFeature]-
The type of the None singleton.
Inherited members
class InputSignUpFeature (form_fields: Union[List[InputFormField], None] = None)-
Expand source code
class InputSignUpFeature: def __init__(self, form_fields: Union[List[InputFormField], None] = None): if form_fields is None: form_fields = [] self.form_fields = normalise_sign_up_form_fields(form_fields) class NormalisedEmailPasswordConfig (**data: Any)-
Base class for normalized config of a Recipe 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.Expand source code
class NormalisedEmailPasswordConfig( BaseNormalisedConfig[RecipeInterface, APIInterface] ): sign_up_feature: SignUpFeature sign_in_feature: SignInFeature reset_password_using_token_feature: ResetPasswordUsingTokenFeature get_email_delivery_config: Callable[ [RecipeInterface], EmailDeliveryConfigWithService[EmailTemplateVars] ]Ancestors
- BaseNormalisedConfig[RecipeInterface, APIInterface]
- BaseNormalisedConfig
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
- typing.Generic
Class variables
var get_email_delivery_config : Callable[[RecipeInterface], EmailDeliveryConfigWithService[PasswordResetEmailTemplateVars]]-
The type of the None singleton.
var reset_password_using_token_feature : ResetPasswordUsingTokenFeature-
The type of the None singleton.
var sign_in_feature : SignInFeature-
The type of the None singleton.
var sign_up_feature : SignUpFeature-
The type of the None singleton.
Inherited members
class ResetPasswordUsingTokenFeature (form_fields_for_password_reset_form: List[NormalisedFormField], form_fields_for_generate_token_form: List[NormalisedFormField])-
Expand source code
class ResetPasswordUsingTokenFeature: def __init__( self, form_fields_for_password_reset_form: List[NormalisedFormField], form_fields_for_generate_token_form: List[NormalisedFormField], ): self.form_fields_for_password_reset_form = form_fields_for_password_reset_form self.form_fields_for_generate_token_form = form_fields_for_generate_token_form class SignInFeature (form_fields: List[NormalisedFormField])-
Expand source code
class SignInFeature: def __init__(self, form_fields: List[NormalisedFormField]): self.form_fields = form_fields class SignUpFeature (form_fields: List[NormalisedFormField])-
Expand source code
class SignUpFeature: def __init__(self, form_fields: List[NormalisedFormField]): self.form_fields = form_fields