Module supertokens_python.recipe.dashboard.api

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 .api_key_protector import api_key_protector
from .dashboard import handle_dashboard_api
from .userdetails.user_delete import handle_user_delete
from .userdetails.user_email_verify_get import handle_user_email_verify_get
from .userdetails.user_email_verify_put import handle_user_email_verify_put
from .userdetails.user_email_verify_token_post import handle_email_verify_token_post
from .userdetails.user_get import handle_user_get
from .userdetails.user_metadata_get import handle_metadata_get
from .userdetails.user_metadata_put import handle_metadata_put
from .userdetails.user_password_put import handle_user_password_put
from .userdetails.user_put import handle_user_put
from .userdetails.user_sessions_get import handle_sessions_get
from .userdetails.user_sessions_post import handle_user_sessions_post
from .users_count_get import handle_users_count_get_api
from .users_get import handle_users_get_api
from .validate_key import handle_validate_key_api

__all__ = [
    "handle_dashboard_api",
    "api_key_protector",
    "handle_users_count_get_api",
    "handle_users_get_api",
    "handle_validate_key_api",
    "handle_user_email_verify_get",
    "handle_user_get",
    "handle_metadata_get",
    "handle_sessions_get",
    "handle_user_delete",
    "handle_user_put",
    "handle_user_email_verify_put",
    "handle_metadata_put",
    "handle_user_sessions_post",
    "handle_user_password_put",
    "handle_email_verify_token_post",
]

Sub-modules

supertokens_python.recipe.dashboard.api.dashboard
supertokens_python.recipe.dashboard.api.implementation
supertokens_python.recipe.dashboard.api.userdetails
supertokens_python.recipe.dashboard.api.users_count_get
supertokens_python.recipe.dashboard.api.users_get
supertokens_python.recipe.dashboard.api.validate_key

Functions

async def api_key_protector(api_implementation: APIInterface, api_options: APIOptions, api_function: Callable[[APIInterface, APIOptions], Awaitable[APIResponse]]) ‑> Optional[BaseResponse]
Expand source code
async def api_key_protector(
    api_implementation: APIInterface,
    api_options: APIOptions,
    api_function: Callable[[APIInterface, APIOptions], Awaitable[APIResponse]],
) -> Optional[BaseResponse]:
    user_context = default_user_context(api_options.request)
    should_allow_access = await api_options.recipe_implementation.should_allow_access(
        api_options.request, api_options.config, user_context
    )

    if should_allow_access is False:
        return send_non_200_response_with_message(
            "Unauthorised access", 401, api_options.response
        )

    response = await api_function(api_implementation, api_options)
    return send_200_response(response.to_json(), api_options.response)
async def handle_dashboard_api(api_implementation: APIInterface, api_options: APIOptions) ‑> Optional[BaseResponse]
Expand source code
async def handle_dashboard_api(
    api_implementation: APIInterface, api_options: APIOptions
) -> Optional[BaseResponse]:
    if api_implementation.dashboard_get is None:
        return None

    user_context = default_user_context(api_options.request)
    html_str = await api_implementation.dashboard_get(api_options, user_context)

    api_options.response.set_html_content(html_str)
    return api_options.response
async def handle_email_verify_token_post(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserEmailVerifyTokenPostAPIOkResponseUserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse]
Expand source code
async def handle_email_verify_token_post(
    _api_interface: APIInterface, api_options: APIOptions
) -> Union[
    UserEmailVerifyTokenPostAPIOkResponse,
    UserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse,
]:
    request_body: Dict[str, Any] = await api_options.request.json()  # type: ignore
    user_id = request_body.get("userId")

    if user_id is None or not isinstance(user_id, str):
        raise_bad_input_exception(
            "Required parameter 'userId' is missing or has an invalid type"
        )

    email_response = await EmailVerificationRecipe.get_instance().get_email_for_user_id(
        user_id, {}
    )

    if not isinstance(email_response, GetEmailForUserIdOkResult):
        raise Exception("Should not come here")

    email_verification_token = await create_email_verification_token(user_id)

    if isinstance(
        email_verification_token, CreateEmailVerificationTokenEmailAlreadyVerifiedError
    ):
        return UserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse()

    assert isinstance(email_verification_token, CreateEmailVerificationTokenOkResult)

    email_verify_link = get_email_verify_link(
        api_options.app_info, email_verification_token.token, user_id
    )

    await send_email(
        VerificationEmailTemplateVars(
            user=VerificationEmailTemplateVarsUser(user_id, email_response.email),
            email_verify_link=email_verify_link,
            user_context={},
        )
    )

    return UserEmailVerifyTokenPostAPIOkResponse()
async def handle_metadata_get(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserMetadataGetAPIOkResponseFeatureNotEnabledError]
Expand source code
async def handle_metadata_get(
    _api_interface: APIInterface, api_options: APIOptions
) -> Union[UserMetadataGetAPIOkResponse, FeatureNotEnabledError]:
    user_id = api_options.request.get_query_param("userId")

    if user_id is None:
        raise_bad_input_exception("Missing required parameter 'userId'")

    try:
        UserMetadataRecipe.get_instance()
    except Exception:
        return FeatureNotEnabledError()

    metadata_response = await get_user_metadata(user_id)
    return UserMetadataGetAPIOkResponse(metadata_response.metadata)
async def handle_metadata_put(_api_interface: APIInterface, api_options: APIOptions) ‑> UserMetadataPutAPIResponse
Expand source code
async def handle_metadata_put(
    _api_interface: APIInterface, api_options: APIOptions
) -> UserMetadataPutAPIResponse:
    request_body: Dict[str, Any] = await api_options.request.json()  # type: ignore
    user_id = request_body.get("userId")
    data = request_body.get("data")

    # This is to throw an error early in case the recipe has not been initialised
    UserMetadataRecipe.get_instance()

    if user_id is None or isinstance(user_id, str) is False:
        raise_bad_input_exception(
            "Required parameter 'userId' is missing or has an invalid type"
        )

    if data is None or isinstance(data, str) is False:
        raise_bad_input_exception(
            "Required parameter 'data' is missing or has an invalid type"
        )

    parsed_data: Dict[str, Any] = {}
    try:
        parsed_data = json.loads(data)
        if not isinstance(parsed_data, dict):  # type: ignore
            raise Exception()

    except Exception:
        raise_bad_input_exception("'data' must be a valid JSON body")

    # This API is meant to set the user metadata of a user. We delete the existing data
    # before updating it because we want to make sure that shallow merging does not result
    # in the data being incorrect
    #
    # For example if the old data is {test: "test", test2: "test2"} and the user wants to delete
    # test2 from the data simply calling updateUserMetadata with {test: "test"} would not remove
    # test2 because of shallow merging.
    #
    # Removing first ensures that the final data is exactly what the user wanted it to be

    await clear_user_metadata(user_id)
    await update_user_metadata(user_id, parsed_data)

    return UserMetadataPutAPIResponse()
async def handle_sessions_get(_api_interface: APIInterface, api_options: APIOptions) ‑> UserSessionsGetAPIResponse
Expand source code
async def handle_sessions_get(
    _api_interface: APIInterface, api_options: APIOptions
) -> UserSessionsGetAPIResponse:
    user_id = api_options.request.get_query_param("userId")

    if user_id is None:
        raise_bad_input_exception("Missing required parameter 'userId'")

    session_handles = await get_all_session_handles_for_user(user_id)
    sessions: List[Optional[SessionInfo]] = [None for _ in session_handles]

    async def call_(i: int, session_handle: str):
        try:
            session_response = await get_session_information(session_handle)
            if session_response is not None:
                sessions[i] = SessionInfo(session_response)
        except Exception:
            sessions[i] = None

    session_info_promises = [
        call_(i, handle) for i, handle in enumerate(session_handles)
    ]

    await asyncio.gather(*session_info_promises)

    return UserSessionsGetAPIResponse([s for s in sessions if s is not None])
async def handle_user_delete(_api_interface: APIInterface, api_options: APIOptions) ‑> UserDeleteAPIResponse
Expand source code
async def handle_user_delete(
    _api_interface: APIInterface, api_options: APIOptions
) -> UserDeleteAPIResponse:
    user_id = api_options.request.get_query_param("userId")

    if user_id is None:
        raise_bad_input_exception("Missing required parameter 'userId'")

    await Supertokens.get_instance().delete_user(user_id)

    return UserDeleteAPIResponse()
async def handle_user_email_verify_get(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserEmailVerifyGetAPIResponseFeatureNotEnabledError]
Expand source code
async def handle_user_email_verify_get(
    _api_interface: APIInterface, api_options: APIOptions
) -> Union[UserEmailVerifyGetAPIResponse, FeatureNotEnabledError]:
    req = api_options.request
    user_id = req.get_query_param("userId")

    if user_id is None:
        raise_bad_input_exception("Missing required parameter 'userId'")

    try:
        EmailVerificationRecipe.get_instance()
    except Exception:
        return FeatureNotEnabledError()

    is_verified = await is_email_verified(user_id)
    return UserEmailVerifyGetAPIResponse(is_verified)
async def handle_user_email_verify_put(_api_interface: APIInterface, api_options: APIOptions) ‑> UserEmailVerifyPutAPIResponse
Expand source code
async def handle_user_email_verify_put(
    _api_interface: APIInterface, api_options: APIOptions
) -> UserEmailVerifyPutAPIResponse:
    request_body: Dict[str, Any] = await api_options.request.json()  # type: ignore
    user_id = request_body.get("userId")
    verified = request_body.get("verified")

    if user_id is None or not isinstance(user_id, str):
        raise_bad_input_exception(
            "Required parameter 'userId' is missing or has an invalid type"
        )

    if verified is None or not isinstance(verified, bool):
        raise_bad_input_exception(
            "Required parameter 'verified' is missing or has an invalid type"
        )

    if verified:
        token_response = await create_email_verification_token(user_id)

        if isinstance(
            token_response, CreateEmailVerificationTokenEmailAlreadyVerifiedError
        ):
            return UserEmailVerifyPutAPIResponse()

        verify_response = await verify_email_using_token(token_response.token)

        if isinstance(verify_response, VerifyEmailUsingTokenInvalidTokenError):
            # This should never happen because we consume the token immediately after creating it
            raise Exception("Should not come here")

    else:
        await unverify_email(user_id)

    return UserEmailVerifyPutAPIResponse()
async def handle_user_get(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserGetAPINoUserFoundErrorUserGetAPIOkResponseUserGetAPIRecipeNotInitialisedError]
Expand source code
async def handle_user_get(
    _api_interface: APIInterface, api_options: APIOptions
) -> Union[
    UserGetAPINoUserFoundError,
    UserGetAPIOkResponse,
    UserGetAPIRecipeNotInitialisedError,
]:
    user_id = api_options.request.get_query_param("userId")
    recipe_id = api_options.request.get_query_param("recipeId")

    if user_id is None:
        raise_bad_input_exception("Missing required parameter 'userId'")

    if recipe_id is None:
        raise_bad_input_exception("Missing required parameter 'recipeId'")

    if not is_valid_recipe_id(recipe_id):
        raise_bad_input_exception("Invalid recipe id")

    if not is_recipe_initialised(recipe_id):
        return UserGetAPIRecipeNotInitialisedError()

    user_response = await get_user_for_recipe_id(user_id, recipe_id)
    if user_response is None:
        return UserGetAPINoUserFoundError()

    user = user_response.user

    try:
        UserMetadataRecipe.get_instance()
    except Exception:
        user.first_name = "FEATURE_NOT_ENABLED"
        user.last_name = "FEATURE_NOT_ENABLED"

        return UserGetAPIOkResponse(recipe_id, user)

    user_metadata = await get_user_metadata(user_id)
    first_name = user_metadata.metadata.get("first_name", "")
    last_name = user_metadata.metadata.get("last_name", "")

    user.first_name = first_name
    user.last_name = last_name

    return UserGetAPIOkResponse(recipe_id, user)
async def handle_user_password_put(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserPasswordPutAPIResponseUserPasswordPutAPIInvalidPasswordErrorResponse]
Expand source code
async def handle_user_password_put(
    _api_interface: APIInterface, api_options: APIOptions
) -> Union[UserPasswordPutAPIResponse, UserPasswordPutAPIInvalidPasswordErrorResponse]:
    request_body: Dict[str, Any] = await api_options.request.json()  # type: ignore
    user_id = request_body.get("userId")
    new_password = request_body.get("newPassword")

    if user_id is None or not isinstance(user_id, str):
        raise_bad_input_exception("Missing required parameter 'userId'")

    if new_password is None or not isinstance(new_password, str):
        raise_bad_input_exception("Missing required parameter 'newPassword'")

    recipe_to_use: Union[
        Literal["emailpassword", "thirdpartyemailpassword"], None
    ] = None

    try:
        EmailPasswordRecipe.get_instance()
        recipe_to_use = "emailpassword"
    except Exception:
        pass

    if recipe_to_use is None:
        try:
            ThirdPartyEmailPasswordRecipe.get_instance()
            recipe_to_use = "thirdpartyemailpassword"
        except Exception:
            pass

    if recipe_to_use is None:
        raise Exception("Should not come here")

    async def reset_password(
        recipe: Any,  # FIXME
        create_reset_password_token: Callable[
            [str],
            Awaitable[
                Union[CreateResetPasswordOkResult, CreateResetPasswordWrongUserIdError]
            ],
        ],
        reset_password_using_token: Callable[
            [str, str],
            Awaitable[
                Union[
                    ResetPasswordUsingTokenOkResult,
                    ResetPasswordUsingTokenInvalidTokenError,
                ]
            ],
        ],
    ) -> Union[
        UserPasswordPutAPIResponse, UserPasswordPutAPIInvalidPasswordErrorResponse
    ]:
        form_fields: List[NormalisedFormField] = recipe.get_instance().config.sign_up_feature.form_fields  # type: ignore # FIXME?
        password_form_field = [
            field for field in form_fields if field.id == FORM_FIELD_PASSWORD_ID
        ][0]

        password_validation_error = await password_form_field.validate(new_password)

        if password_validation_error is not None:
            return UserPasswordPutAPIInvalidPasswordErrorResponse(
                password_validation_error
            )

        password_reset_token = await create_reset_password_token(user_id)  # type: ignore # FIXME

        if isinstance(password_reset_token, CreateResetPasswordWrongUserIdError):
            # Techincally it can but its an edge case so we assume that it wont
            # UNKNOWN_USER_ID_ERROR FIXME
            raise Exception("Should never come here")

        password_reset_response = await reset_password_using_token(
            password_reset_token.token, new_password
        )

        if isinstance(
            password_reset_response, ResetPasswordUsingTokenInvalidTokenError
        ):
            # RESET_PASSWORD_INVALID_TOKEN_ERROR
            raise Exception("Should not come here")

        return UserPasswordPutAPIResponse()

    if recipe_to_use == "emailpassword":
        return await reset_password(
            EmailPasswordRecipe,
            ep_create_reset_password_token,
            ep_reset_password_using_token,
        )

    if recipe_to_use == "thirdpartyemailpassword":
        return await reset_password(
            ThirdPartyEmailPasswordRecipe,
            tpep_create_reset_password_token,
            tpep_reset_password_using_token,
        )
async def handle_user_put(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserPutAPIOkResponseUserPutAPIInvalidEmailErrorResponseUserPutAPIEmailAlreadyExistsErrorResponseUserPutAPIInvalidPhoneErrorResponseUserPutPhoneAlreadyExistsAPIResponse]
Expand source code
async def handle_user_put(
    _api_interface: APIInterface, api_options: APIOptions
) -> Union[
    UserPutAPIOkResponse,
    UserPutAPIInvalidEmailErrorResponse,
    UserPutAPIEmailAlreadyExistsErrorResponse,
    UserPutAPIInvalidPhoneErrorResponse,
    UserPutPhoneAlreadyExistsAPIResponse,
]:
    request_body: Dict[str, Any] = await api_options.request.json()  # type: ignore
    user_id: Optional[str] = request_body.get("userId")
    recipe_id: Optional[str] = request_body.get("recipeId")
    first_name: Optional[str] = request_body.get("firstName")
    last_name: Optional[str] = request_body.get("lastName")
    email: Optional[str] = request_body.get("email")
    phone: Optional[str] = request_body.get("phone")

    if not isinstance(user_id, str):
        return raise_bad_input_exception(
            "Required parameter 'userId' is missing or has an invalid type"
        )

    if not isinstance(recipe_id, str):
        return raise_bad_input_exception(
            "Required parameter 'recipeId' is missing or has an invalid type"
        )

    if not is_valid_recipe_id(recipe_id):
        raise_bad_input_exception("Invalid recipe id")

    if first_name is None and not isinstance(first_name, str):
        raise_bad_input_exception(
            "Required parameter 'firstName' is missing or has an invalid type"
        )

    if last_name is None and not isinstance(last_name, str):
        raise_bad_input_exception(
            "Required parameter 'lastName' is missing or has an invalid type"
        )

    if email is None and not isinstance(email, str):
        raise_bad_input_exception(
            "Required parameter 'email' is missing or has an invalid type"
        )

    if phone is None and not isinstance(phone, str):
        raise_bad_input_exception(
            "Required parameter 'phone' is missing or has an invalid type"
        )

    user_response = await get_user_for_recipe_id(user_id, recipe_id)

    if user_response is None:
        raise Exception("Should never come here")

    first_name = first_name.strip()
    last_name = last_name.strip()
    email = email.strip()
    phone = phone.strip()

    if first_name != "" or last_name != "":
        is_recipe_initialized = False

        try:
            UserMetadataRecipe.get_instance()
            is_recipe_initialized = True
        except Exception:
            pass

        if is_recipe_initialized:
            metadata_update = {}

            if first_name != "":
                metadata_update["first_name"] = first_name

            if last_name != "":
                metadata_update["last_name"] = last_name

            await update_user_metadata(user_id, metadata_update)

    if email != "":
        email_update_response = await update_email_for_recipe_id(
            user_response.recipe, user_id, email
        )

        if not isinstance(email_update_response, UserPutAPIOkResponse):
            return email_update_response

    if phone != "":
        phone_update_response = await update_phone_for_recipe_id(
            user_response.recipe, user_id, phone
        )

        if not isinstance(phone_update_response, UserPutAPIOkResponse):
            return phone_update_response

    return UserPutAPIOkResponse()
async def handle_user_sessions_post(_api_interface: APIInterface, api_options: APIOptions) ‑> UserSessionsPostAPIResponse
Expand source code
async def handle_user_sessions_post(
    _api_interface: APIInterface, api_options: APIOptions
) -> UserSessionsPostAPIResponse:
    request_body: Dict[str, Any] = await api_options.request.json()  # type: ignore
    session_handles: Union[List[str], Any] = request_body.get("sessionHandles")

    if session_handles is None or not isinstance(session_handles, list):
        return raise_bad_input_exception(
            "Required parameter 'sessionHandles' is missing or has an invalid type"
        )

    await revoke_multiple_sessions(session_handles)
    return UserSessionsPostAPIResponse()
async def handle_users_count_get_api(_: APIInterface, _api_options: APIOptions) ‑> UserCountGetAPIResponse
Expand source code
async def handle_users_count_get_api(
    _: APIInterface, _api_options: APIOptions
) -> UserCountGetAPIResponse:
    count = await Supertokens.get_instance().get_user_count(include_recipe_ids=None)
    return UserCountGetAPIResponse(count=count)
async def handle_users_get_api(api_implementation: APIInterface, api_options: APIOptions) ‑> APIResponse
Expand source code
async def handle_users_get_api(
    api_implementation: APIInterface, api_options: APIOptions
) -> APIResponse:
    _ = api_implementation

    limit = api_options.request.get_query_param("limit")
    if limit is None:
        raise_bad_input_exception("Missing required parameter 'limit'")

    time_joined_order: str = api_options.request.get_query_param(  # type: ignore
        "timeJoinedOrder", "DESC"
    )
    if time_joined_order not in ["ASC", "DESC"]:
        raise_bad_input_exception("Invalid value recieved for 'timeJoinedOrder'")

    pagination_token = api_options.request.get_query_param("paginationToken")

    users_response = await Supertokens.get_instance().get_users(
        limit=int(limit),
        time_joined_order=time_joined_order,  # type: ignore
        pagination_token=pagination_token,
        include_recipe_ids=None,
    )

    # user metadata bulk fetch with batches:

    try:
        UserMetadataRecipe.get_instance()
    except GeneralError:
        return DashboardUsersGetResponse(
            users_response.users, users_response.next_pagination_token
        )

    users_with_metadata: List[UserWithMetadata] = [
        UserWithMetadata().from_user(user) for user in users_response.users
    ]
    metadata_fetch_awaitables: List[Awaitable[Any]] = []

    async def get_user_metadata_and_update_user(user_idx: int) -> None:
        user = users_response.users[user_idx]
        user_metadata = await get_user_metadata(user.user_id)
        first_name = user_metadata.metadata.get("first_name")
        last_name = user_metadata.metadata.get("last_name")

        # None becomes null which is acceptable for the dashboard.
        users_with_metadata[user_idx].first_name = first_name
        users_with_metadata[user_idx].last_name = last_name

    # Batch calls to get user metadata:
    for i, _ in enumerate(users_response.users):
        metadata_fetch_awaitables.append(get_user_metadata_and_update_user(i))

    promise_arr_start_position = 0
    batch_size = 5

    while promise_arr_start_position < len(metadata_fetch_awaitables):
        # We want to query only 5 in parallel at a time
        promises_to_call = [
            metadata_fetch_awaitables[i]
            for i in range(
                promise_arr_start_position,
                min(
                    promise_arr_start_position + batch_size,
                    len(metadata_fetch_awaitables),
                ),
            )
        ]
        await asyncio.gather(*promises_to_call)

        promise_arr_start_position += batch_size

    return DashboardUsersGetResponse(
        users_with_metadata,
        users_response.next_pagination_token,
    )
async def handle_validate_key_api(api_implementation: APIInterface, api_options: APIOptions)
Expand source code
async def handle_validate_key_api(
    api_implementation: APIInterface, api_options: APIOptions
):
    _ = api_implementation

    should_allow_accesss = await api_options.recipe_implementation.should_allow_access(
        api_options.request,
        api_options.config,
        default_user_context(api_options.request),
    )
    if should_allow_accesss is False:
        return send_non_200_response_with_message(
            "Unauthorized access", 401, api_options.response
        )

    return send_200_response({"status": "OK"}, api_options.response)