Module supertokens_python.recipe.dashboard.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 typing import TYPE_CHECKING, Any, Dict, List, Optional
from typing_extensions import Literal
from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe
from supertokens_python.recipe.webauthn.recipe import WebauthnRecipe
from supertokens_python.types.config import (
BaseConfig,
BaseNormalisedConfig,
BaseNormalisedOverrideConfig,
BaseOverrideableConfig,
BaseOverrideConfig,
)
if TYPE_CHECKING:
from supertokens_python.framework.request import BaseRequest
from supertokens_python.recipe.emailpassword import EmailPasswordRecipe
from supertokens_python.recipe.passwordless import PasswordlessRecipe
from supertokens_python.recipe.thirdparty import ThirdPartyRecipe
from supertokens_python.types import RecipeUserId, User
from supertokens_python.utils import log_debug_message, normalise_email
from ...normalised_url_path import NormalisedURLPath
from .constants import (
DASHBOARD_ANALYTICS_API,
DASHBOARD_API,
SEARCH_TAGS_API,
SIGN_IN_API,
SIGN_OUT_API,
USER_API,
USER_EMAIL_VERIFY_API,
USER_EMAIL_VERIFY_TOKEN_API,
USER_METADATA_API,
USER_PASSWORD_API,
USER_SESSION_API,
USERS_COUNT_API,
USERS_LIST_GET_API,
VALIDATE_KEY_API,
)
from .interfaces import APIInterface, RecipeInterface
class UserWithMetadata:
user: User
first_name: Optional[str] = None
last_name: Optional[str] = None
def from_user(
self,
user: User,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
):
self.first_name = first_name or ""
self.last_name = last_name or ""
self.user = user
return self
def to_json(self) -> Dict[str, Any]:
user_json = self.user.to_json()
user_json["firstName"] = self.first_name
user_json["lastName"] = self.last_name
return user_json
DashboardOverrideConfig = BaseOverrideConfig[RecipeInterface, APIInterface]
NormalisedDashboardOverrideConfig = BaseNormalisedOverrideConfig[
RecipeInterface, APIInterface
]
InputOverrideConfig = DashboardOverrideConfig
"""Deprecated, use `DashboardOverrideConfig` instead."""
class DashboardOverrideableConfig(BaseOverrideableConfig):
"""Input config properties overrideable using the plugin config overrides"""
api_key: Optional[str] = None
admins: Optional[List[str]] = None
class DashboardConfig(
DashboardOverrideableConfig,
BaseConfig[RecipeInterface, APIInterface, DashboardOverrideableConfig],
):
def to_overrideable_config(self) -> DashboardOverrideableConfig:
"""Create a `DashboardOverrideableConfig` from the current config."""
return DashboardOverrideableConfig(**self.model_dump())
def from_overrideable_config(
self,
overrideable_config: DashboardOverrideableConfig,
) -> "DashboardConfig":
"""
Create a `DashboardConfig` from a `DashboardOverrideableConfig`.
Not a classmethod since it needs to be used in a dynamic context within plugins.
"""
return DashboardConfig(
**overrideable_config.model_dump(),
override=self.override,
)
class NormalisedDashboardConfig(BaseNormalisedConfig[RecipeInterface, APIInterface]):
api_key: Optional[str]
admins: Optional[List[str]]
auth_mode: str
def validate_and_normalise_user_input(
config: DashboardConfig,
) -> NormalisedDashboardConfig:
override_config = NormalisedDashboardOverrideConfig.from_input_config(
override_config=config.override
)
if config.api_key is not None and config.admins is not None:
log_debug_message(
"User Dashboard: Providing 'admins' has no effect when using an api key."
)
admins = (
[normalise_email(a) for a in config.admins]
if config.admins is not None
else None
)
auth_mode = "api-key" if config.api_key else "email-password"
return NormalisedDashboardConfig(
api_key=config.api_key,
admins=admins,
auth_mode=auth_mode,
override=override_config,
)
def is_api_path(path: NormalisedURLPath, base_path: NormalisedURLPath) -> bool:
dashboard_recipe_base_path = base_path.append(NormalisedURLPath(DASHBOARD_API))
if not path.startswith(dashboard_recipe_base_path):
return False
path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[1]
if len(path_without_dashboard_path) > 0 and path_without_dashboard_path[0] == "/":
path_without_dashboard_path = path_without_dashboard_path[1:]
if path_without_dashboard_path.split("/")[0] == "api":
return True
return False
def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]:
path_str = path.get_as_string_dangerous()
if path_str.endswith(VALIDATE_KEY_API) and method == "post":
return VALIDATE_KEY_API
if path_str.endswith(USERS_LIST_GET_API) and method == "get":
return USERS_LIST_GET_API
if path_str.endswith(USERS_COUNT_API) and method == "get":
return USERS_COUNT_API
if path_str.endswith(USER_API) and method in ("get", "delete", "put"):
return USER_API
if path_str.endswith(USER_EMAIL_VERIFY_API) and method in ("get", "put"):
return USER_EMAIL_VERIFY_API
if path_str.endswith(USER_METADATA_API) and method in ("get", "put"):
return USER_METADATA_API
if path_str.endswith(USER_SESSION_API) and method in ("get", "post"):
return USER_SESSION_API
if path_str.endswith(USER_PASSWORD_API) and method == "put":
return USER_PASSWORD_API
if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post":
return USER_EMAIL_VERIFY_TOKEN_API
if path_str.endswith(SIGN_IN_API) and method == "post":
return SIGN_IN_API
if path_str.endswith(SIGN_OUT_API) and method == "post":
return SIGN_OUT_API
if path_str.endswith(SEARCH_TAGS_API) and method == "get":
return SEARCH_TAGS_API
if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post":
return DASHBOARD_ANALYTICS_API
return None
class GetUserForRecipeIdHelperResult:
def __init__(self, user: Optional[User] = None, recipe: Optional[str] = None):
self.user = user
self.recipe = recipe
class GetUserForRecipeIdResult:
def __init__(
self, user: Optional[UserWithMetadata] = None, recipe: Optional[str] = None
):
self.user = user
self.recipe = recipe
async def get_user_for_recipe_id(
recipe_user_id: RecipeUserId, recipe_id: str, user_context: Dict[str, Any]
) -> GetUserForRecipeIdResult:
user_response = await _get_user_for_recipe_id(
recipe_user_id, recipe_id, user_context
)
user = None
if user_response.user is not None:
user = UserWithMetadata().from_user(
user_response.user, first_name="", last_name=""
)
return GetUserForRecipeIdResult(user=user, recipe=user_response.recipe)
async def _get_user_for_recipe_id(
recipe_user_id: RecipeUserId, recipe_id: str, user_context: Dict[str, Any]
) -> GetUserForRecipeIdHelperResult:
recipe: Optional[
Literal["emailpassword", "thirdparty", "passwordless", "webauthn"]
] = None
user = await AccountLinkingRecipe.get_instance().recipe_implementation.get_user(
recipe_user_id.get_as_string(), user_context
)
if user is None:
return GetUserForRecipeIdHelperResult(user=None, recipe=None)
login_method = next(
(
m
for m in user.login_methods
if m.recipe_id == recipe_id
and m.recipe_user_id.get_as_string() == recipe_user_id.get_as_string()
),
None,
)
if login_method is None:
return GetUserForRecipeIdHelperResult(user=None, recipe=None)
if recipe_id == EmailPasswordRecipe.recipe_id:
try:
EmailPasswordRecipe.get_instance()
recipe = "emailpassword"
except Exception:
pass
elif recipe_id == ThirdPartyRecipe.recipe_id:
try:
ThirdPartyRecipe.get_instance()
recipe = "thirdparty"
except Exception:
pass
elif recipe_id == PasswordlessRecipe.recipe_id:
try:
PasswordlessRecipe.get_instance()
recipe = "passwordless"
except Exception:
pass
elif recipe_id == WebauthnRecipe.recipe_id:
try:
WebauthnRecipe.get_instance()
recipe = "webauthn"
except Exception:
pass
return GetUserForRecipeIdHelperResult(user=user, recipe=recipe)
async def validate_api_key(
req: BaseRequest, config: NormalisedDashboardConfig, _user_context: Dict[str, Any]
) -> bool:
api_key_header_value = req.get_header("authorization")
if not api_key_header_value:
return False
# We receieve the api key as `Bearer API_KEY`, this retrieves just the key
api_key_header_value = api_key_header_value.split(" ")[1]
return api_key_header_value == config.api_key
def get_api_path_with_dashboard_base(path: str) -> str:
return DASHBOARD_API + path
Functions
def get_api_if_matched(path: NormalisedURLPath, method: str) ‑> Optional[str]def get_api_path_with_dashboard_base(path: str) ‑> strasync def get_user_for_recipe_id(recipe_user_id: RecipeUserId, recipe_id: str, user_context: Dict[str, Any]) ‑> GetUserForRecipeIdResultdef is_api_path(path: NormalisedURLPath, base_path: NormalisedURLPath) ‑> booldef validate_and_normalise_user_input(config: DashboardConfig) ‑> NormalisedDashboardConfigasync def validate_api_key(req: BaseRequest, config: NormalisedDashboardConfig, _user_context: Dict[str, Any])
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 DashboardConfig (**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 DashboardConfig( DashboardOverrideableConfig, BaseConfig[RecipeInterface, APIInterface, DashboardOverrideableConfig], ): def to_overrideable_config(self) -> DashboardOverrideableConfig: """Create a `DashboardOverrideableConfig` from the current config.""" return DashboardOverrideableConfig(**self.model_dump()) def from_overrideable_config( self, overrideable_config: DashboardOverrideableConfig, ) -> "DashboardConfig": """ Create a `DashboardConfig` from a `DashboardOverrideableConfig`. Not a classmethod since it needs to be used in a dynamic context within plugins. """ return DashboardConfig( **overrideable_config.model_dump(), override=self.override, )Ancestors
- DashboardOverrideableConfig
- BaseOverrideableConfig
- supertokens_python.types.config.BaseConfig[RecipeInterface, APIInterface, DashboardOverrideableConfig]
- BaseConfig
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
- typing.Generic
Methods
def from_overrideable_config(self, overrideable_config: DashboardOverrideableConfig) ‑> DashboardConfig-
Create a
DashboardConfigfrom aDashboardOverrideableConfig. Not a classmethod since it needs to be used in a dynamic context within plugins. def to_overrideable_config(self) ‑> DashboardOverrideableConfig-
Create a
DashboardOverrideableConfigfrom the current config.
Inherited members
class DashboardOverrideableConfig (**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 DashboardOverrideableConfig(BaseOverrideableConfig): """Input config properties overrideable using the plugin config overrides""" api_key: Optional[str] = None admins: Optional[List[str]] = NoneAncestors
- BaseOverrideableConfig
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
Subclasses
Class variables
var admins : Optional[List[str]]-
The type of the None singleton.
var api_key : Optional[str]-
The type of the None singleton.
Inherited members
class GetUserForRecipeIdHelperResult (user: Optional[User] = None, recipe: Optional[str] = None)-
Expand source code
class GetUserForRecipeIdHelperResult: def __init__(self, user: Optional[User] = None, recipe: Optional[str] = None): self.user = user self.recipe = recipe class GetUserForRecipeIdResult (user: Optional[UserWithMetadata] = None, recipe: Optional[str] = None)-
Expand source code
class GetUserForRecipeIdResult: def __init__( self, user: Optional[UserWithMetadata] = None, recipe: Optional[str] = None ): self.user = user self.recipe = recipe class NormalisedDashboardConfig (**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 NormalisedDashboardConfig(BaseNormalisedConfig[RecipeInterface, APIInterface]): api_key: Optional[str] admins: Optional[List[str]] auth_mode: strAncestors
- BaseNormalisedConfig[RecipeInterface, APIInterface]
- BaseNormalisedConfig
- CamelCaseBaseModel
- APIResponse
- abc.ABC
- pydantic.main.BaseModel
- typing.Generic
Class variables
var admins : Optional[List[str]]-
The type of the None singleton.
var api_key : Optional[str]-
The type of the None singleton.
var auth_mode : str-
The type of the None singleton.
Inherited members
class UserWithMetadata-
Expand source code
class UserWithMetadata: user: User first_name: Optional[str] = None last_name: Optional[str] = None def from_user( self, user: User, first_name: Optional[str] = None, last_name: Optional[str] = None, ): self.first_name = first_name or "" self.last_name = last_name or "" self.user = user return self def to_json(self) -> Dict[str, Any]: user_json = self.user.to_json() user_json["firstName"] = self.first_name user_json["lastName"] = self.last_name return user_jsonClass variables
var first_name : Optional[str]-
The type of the None singleton.
var last_name : Optional[str]-
The type of the None singleton.
var user : User-
The type of the None singleton.
Methods
def from_user(self, user: User, first_name: Optional[str] = None, last_name: Optional[str] = None)def to_json(self) ‑> Dict[str, Any]