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[UserEmailVerifyTokenPostAPIOkResponse, UserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse]
-
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[UserMetadataGetAPIOkResponse, FeatureNotEnabledError]
-
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[UserEmailVerifyGetAPIResponse, FeatureNotEnabledError]
-
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[UserGetAPINoUserFoundError, UserGetAPIOkResponse, UserGetAPIRecipeNotInitialisedError]
-
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[UserPasswordPutAPIResponse, UserPasswordPutAPIInvalidPasswordErrorResponse]
-
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[UserPutAPIOkResponse, UserPutAPIInvalidEmailErrorResponse, UserPutAPIEmailAlreadyExistsErrorResponse, UserPutAPIInvalidPhoneErrorResponse, UserPutPhoneAlreadyExistsAPIResponse]
-
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)