diff --git a/supertokens_python/recipe/accountlinking/interfaces.py b/supertokens_python/recipe/accountlinking/interfaces.py index 8b28e491..7ee318f9 100644 --- a/supertokens_python/recipe/accountlinking/interfaces.py +++ b/supertokens_python/recipe/accountlinking/interfaces.py @@ -220,9 +220,9 @@ def __init__(self, accounts_already_linked: bool, user: User): class LinkAccountsRecipeUserIdAlreadyLinkedError: def __init__( self, - primary_user_id: Optional[str] = None, - user: Optional[User] = None, - description: Optional[str] = None, + primary_user_id: str, + user: User, + description: str, ): self.status: Literal[ "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" @@ -236,14 +236,12 @@ class LinkAccountsAccountInfoAlreadyAssociatedError: def __init__( self, primary_user_id: Optional[str] = None, - user: Optional[User] = None, description: Optional[str] = None, ): self.status: Literal[ "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" ] = "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" self.primary_user_id = primary_user_id - self.user = user self.description = description diff --git a/supertokens_python/recipe/accountlinking/recipe_implementation.py b/supertokens_python/recipe/accountlinking/recipe_implementation.py index c9125b2f..ef156e39 100644 --- a/supertokens_python/recipe/accountlinking/recipe_implementation.py +++ b/supertokens_python/recipe/accountlinking/recipe_implementation.py @@ -278,6 +278,7 @@ async def link_accounts( ): return LinkAccountsRecipeUserIdAlreadyLinkedError( primary_user_id=response["primaryUserId"], + user=response["user"], description=response["description"], ) elif ( diff --git a/supertokens_python/recipe/accountlinking/syncio/__init__.py b/supertokens_python/recipe/accountlinking/syncio/__init__.py index 9153a612..6de893c1 100644 --- a/supertokens_python/recipe/accountlinking/syncio/__init__.py +++ b/supertokens_python/recipe/accountlinking/syncio/__init__.py @@ -15,7 +15,8 @@ from supertokens_python.async_to_sync_wrapper import sync -from ..types import AccountInfoWithRecipeId, User, RecipeUserId +from ..types import AccountInfoWithRecipeId +from supertokens_python.types import RecipeUserId from supertokens_python.recipe.session import SessionContainer @@ -24,7 +25,7 @@ def create_primary_user_id_or_link_accounts( recipe_user_id: RecipeUserId, session: Optional[SessionContainer] = None, user_context: Optional[Dict[str, Any]] = None, -) -> User: +): from ..asyncio import ( create_primary_user_id_or_link_accounts as async_create_primary_user_id_or_link_accounts, ) @@ -40,7 +41,7 @@ def get_primary_user_that_can_be_linked_to_recipe_user_id( tenant_id: str, recipe_user_id: RecipeUserId, user_context: Optional[Dict[str, Any]] = None, -) -> Optional[User]: +): from ..asyncio import ( get_primary_user_that_can_be_linked_to_recipe_user_id as async_get_primary_user_that_can_be_linked_to_recipe_user_id, ) diff --git a/supertokens_python/recipe/session/utils.py b/supertokens_python/recipe/session/utils.py index 7c5f1efd..13f3d8dc 100644 --- a/supertokens_python/recipe/session/utils.py +++ b/supertokens_python/recipe/session/utils.py @@ -567,7 +567,7 @@ def anti_csrf_function( ( overwrite_session_during_sign_in_up if overwrite_session_during_sign_in_up is not None - else True + else False ), ) diff --git a/tests/test-server/accountlinking.py b/tests/test-server/accountlinking.py new file mode 100644 index 00000000..8a17e40b --- /dev/null +++ b/tests/test-server/accountlinking.py @@ -0,0 +1,284 @@ +from flask import Flask, request, jsonify +from supertokens_python import async_to_sync_wrapper, convert_to_recipe_user_id +from supertokens_python.recipe.accountlinking.syncio import can_create_primary_user +from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe +from supertokens_python.recipe.accountlinking.syncio import is_sign_in_allowed +from supertokens_python.recipe.accountlinking.syncio import is_sign_up_allowed +from supertokens_python.recipe.accountlinking.syncio import ( + get_primary_user_that_can_be_linked_to_recipe_user_id, +) +from supertokens_python.recipe.accountlinking.syncio import ( + create_primary_user_id_or_link_accounts, +) +from supertokens_python.recipe.accountlinking.syncio import unlink_account +from supertokens_python.recipe.accountlinking.syncio import is_email_change_allowed +from supertokens_python.recipe.accountlinking.syncio import ( + link_accounts, + create_primary_user, +) +from supertokens_python.recipe.accountlinking.interfaces import ( + CanCreatePrimaryUserOkResult, + CanCreatePrimaryUserRecipeUserIdAlreadyLinkedError, + CreatePrimaryUserOkResult, + CreatePrimaryUserRecipeUserIdAlreadyLinkedError, + LinkAccountsAccountInfoAlreadyAssociatedError, + LinkAccountsOkResult, + LinkAccountsRecipeUserIdAlreadyLinkedError, +) +from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeId +from supertokens_python.recipe.thirdparty.types import ThirdPartyInfo +from supertokens_python.types import User +from utils import serialize_user # pylint: disable=import-error + + +def add_accountlinking_routes(app: Flask): + @app.route("/test/accountlinking/createprimaryuser", methods=["POST"]) # type: ignore + def create_primary_user_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = create_primary_user( + recipe_user_id, request.json.get("userContext") + ) + if isinstance(response, CreatePrimaryUserOkResult): + return jsonify( + { + "status": "OK", + **serialize_user( + response.user, request.headers.get("fdi-version", "") + ), + "wasAlreadyAPrimaryUser": response.was_already_a_primary_user, + } + ) + elif isinstance(response, CreatePrimaryUserRecipeUserIdAlreadyLinkedError): + return jsonify( + { + "description": response.description, + "primaryUserId": response.primary_user_id, + "status": response.status, + } + ) + elif isinstance(response, CreatePrimaryUserRecipeUserIdAlreadyLinkedError): + return jsonify( + { + "description": response.description, + "primaryUserId": response.primary_user_id, + "status": response.status, + } + ) + else: + return jsonify( + { + "description": response.description, + "primaryUserId": response.primary_user_id, + "status": response.status, + } + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/linkaccounts", methods=["POST"]) # type: ignore + def link_accounts_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = link_accounts( + recipe_user_id, + request.json["primaryUserId"], + request.json.get("userContext"), + ) + if isinstance(response, LinkAccountsOkResult): + return jsonify( + { + "status": "OK", + **serialize_user( + response.user, request.headers.get("fdi-version", "") + ), + "accountsAlreadyLinked": response.accounts_already_linked, + } + ) + elif isinstance(response, LinkAccountsRecipeUserIdAlreadyLinkedError): + return jsonify( + { + "description": response.description, + "primaryUserId": response.primary_user_id, + "status": response.status, + **serialize_user( + response.user, request.headers.get("fdi-version", "") + ), + } + ) + elif isinstance(response, LinkAccountsAccountInfoAlreadyAssociatedError): + return jsonify( + { + "description": response.description, + "primaryUserId": response.primary_user_id, + "status": response.status, + } + ) + else: + return jsonify( + { + "description": response.description, + "status": response.status, + } + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/isemailchangeallowed", methods=["POST"]) # type: ignore + def is_email_change_allowed_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = is_email_change_allowed( + recipe_user_id, + request.json["newEmail"], + request.json["isVerified"], + request.json["session"], + request.json.get("userContext"), + ) + return jsonify(response) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/unlinkaccount", methods=["POST"]) # type: ignore + def unlink_account_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = unlink_account( + recipe_user_id, + request.json.get("userContext"), + ) + return jsonify( + { + "status": response.status, + "wasRecipeUserDeleted": response.was_recipe_user_deleted, + "wasLinked": response.was_linked, + } + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/createprimaryuseridorlinkaccounts", methods=["POST"]) # type: ignore + def create_primary_user_id_or_link_accounts_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = create_primary_user_id_or_link_accounts( + request.json["tenantId"], + recipe_user_id, + request.json.get("session", None), + request.json.get("userContext", None), + ) + return jsonify(response.to_json()) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/getprimaryuserthatcanbelinkedtorecipeuserid", methods=["POST"]) # type: ignore + def get_primary_user_that_can_be_linked_to_recipe_user_id_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = get_primary_user_that_can_be_linked_to_recipe_user_id( + request.json["tenantId"], + recipe_user_id, + request.json.get("userContext", None), + ) + return jsonify(response.to_json() if response else None) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/issignupallowed", methods=["POST"]) # type: ignore + def is_signup_allowed_api(): # type: ignore + try: + assert request.json is not None + response = is_sign_up_allowed( + request.json["tenantId"], + AccountInfoWithRecipeId( + recipe_id=request.json["newUser"]["recipeId"], + email=request.json["newUser"]["email"], + phone_number=request.json["newUser"]["phoneNumber"], + third_party=ThirdPartyInfo( + third_party_user_id=request.json["newUser"]["thirdParty"]["id"], + third_party_id=request.json["newUser"]["thirdParty"][ + "thirdPartyId" + ], + ), + ), + request.json["isVerified"], + request.json.get("session", None), + request.json.get("userContext", None), + ) + return jsonify(response) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/issigninallowed", methods=["POST"]) # type: ignore + def is_signin_allowed_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = is_sign_in_allowed( + request.json["tenantId"], + recipe_user_id, + request.json.get("session", None), + request.json.get("userContext", None), + ) + return jsonify(response) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/verifyemailforrecipeuseriflinkedaccountsareverified", methods=["POST"]) # type: ignore + def verify_email_for_recipe_user_if_linked_accounts_are_verified_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + user = User.from_json(request.json["user"]) + async_to_sync_wrapper.sync( + AccountLinkingRecipe.get_instance().verify_email_for_recipe_user_if_linked_accounts_are_verified( + user=user, + recipe_user_id=recipe_user_id, + user_context=request.json.get("userContext"), + ) + ) + return jsonify({}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/test/accountlinking/cancreateprimaryuser", methods=["POST"]) # type: ignore + def can_create_primary_user_api(): # type: ignore + try: + assert request.json is not None + recipe_user_id = convert_to_recipe_user_id(request.json["recipeUserId"]) + response = can_create_primary_user( + recipe_user_id, request.json.get("userContext") + ) + if isinstance(response, CanCreatePrimaryUserOkResult): + return jsonify( + { + "status": response.status, + "wasAlreadyAPrimaryUser": response.was_already_a_primary_user, + } + ) + elif isinstance( + response, CanCreatePrimaryUserRecipeUserIdAlreadyLinkedError + ): + return jsonify( + { + "description": response.description, + "primaryUserId": response.primary_user_id, + "status": response.status, + } + ) + else: + return jsonify( + { + "description": response.description, + "status": response.status, + "primaryUserId": response.primary_user_id, + } + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/tests/test-server/app.py b/tests/test-server/app.py index 87a63489..713debd9 100644 --- a/tests/test-server/app.py +++ b/tests/test-server/app.py @@ -1,3 +1,4 @@ +import inspect from typing import Any, Callable, Dict, List, Optional, TypeVar, Tuple from flask import Flask, request, jsonify from supertokens_python.framework import BaseRequest @@ -24,7 +25,9 @@ reset_override_params, ) # pylint: disable=import-error from emailpassword import add_emailpassword_routes # pylint: disable=import-error +from thirdparty import add_thirdparty_routes # pylint: disable=import-error from multitenancy import add_multitenancy_routes # pylint: disable=import-error +from accountlinking import add_accountlinking_routes # pylint: disable=import-error from emailverification import ( add_emailverification_routes, ) # pylint: disable=import-error @@ -105,7 +108,10 @@ async def finalFunction(*args: Any, **kwargs: Any): {"args": args, "kwargs": kwargs}, ) try: - res = await originalFunction(*args, **kwargs) + if inspect.iscoroutinefunction(originalFunction): + res = await originalFunction(*args, **kwargs) + else: + res = originalFunction(*args, **kwargs) override_logging.log_override_event( name + "." + toCamelCase(functionName), "RES", res ) @@ -256,6 +262,9 @@ def init_st(config: Dict[str, Any]): use_dynamic_access_token_signing_key=recipe_config_json.get( "useDynamicAccessTokenSigningKey" ), + overwrite_session_during_sign_in_up=recipe_config_json.get( + "overwriteSessionDuringSignInUp", None + ), override=session.InputOverrideConfig( apis=override_builder_with_logging( "Session.override.apis", @@ -440,6 +449,14 @@ def inner( body: Optional[Dict[str, Any]], user_context: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: + # print( + # "-------------------------------------------!!!!!!!!!!!!!!!!!!!!!!!!!!!" + # ) + # print(url) + # import traceback + # print("Stack trace:") + # traceback.print_stack() + if interceptor_func is not None: resp = interceptor_func( url, method, headers, params, body, user_context @@ -571,6 +588,8 @@ def not_found(error: Any) -> Any: # pylint: disable=unused-argument add_multitenancy_routes(app) add_session_routes(app) add_emailverification_routes(app) +add_thirdparty_routes(app) +add_accountlinking_routes(app) init_test_claims() diff --git a/tests/test-server/thirdparty.py b/tests/test-server/thirdparty.py new file mode 100644 index 00000000..dfc0ff00 --- /dev/null +++ b/tests/test-server/thirdparty.py @@ -0,0 +1,70 @@ +from flask import Flask, request, jsonify + +from session import convert_session_to_container # pylint: disable=import-error +from supertokens_python.recipe.thirdparty.interfaces import ( + EmailChangeNotAllowedError, + ManuallyCreateOrUpdateUserOkResult, + SignInUpNotAllowed, +) +from supertokens_python.recipe.thirdparty.syncio import manually_create_or_update_user +from utils import ( # pylint: disable=import-error + serialize_user, + serialize_recipe_user_id, +) # pylint: disable=import-error + + +def add_thirdparty_routes(app: Flask): + @app.route("/test/thirdparty/manuallycreateorupdateuser", methods=["POST"]) # type: ignore + def thirdpartymanuallycreateorupdate(): # type: ignore + data = request.json + if data is None: + return jsonify({"status": "MISSING_DATA_ERROR"}) + + tenant_id = data.get("tenantId", "public") + third_party_id = data["thirdPartyId"] + third_party_user_id = data["thirdPartyUserId"] + email = data["email"] + is_verified = data["isVerified"] + user_context = data.get("userContext", {}) + + session = None + if data.get("session"): + session = convert_session_to_container(data["session"]) + + response = manually_create_or_update_user( + tenant_id, + third_party_id, + third_party_user_id, + email, + is_verified, + session, + user_context, + ) + + if isinstance(response, ManuallyCreateOrUpdateUserOkResult): + return jsonify( + { + "status": "OK", + **serialize_user( + response.user, request.headers.get("fdi-version", "") + ), + **serialize_recipe_user_id( + response.recipe_user_id, request.headers.get("fdi-version", "") + ), + } + ) + elif isinstance(response, EmailChangeNotAllowedError): + return jsonify( + {"status": "EMAIL_CHANGE_NOT_ALLOWED_ERROR", "reason": response.reason} + ) + elif isinstance(response, SignInUpNotAllowed): + return jsonify(response.to_json()) + elif isinstance(response, SignInUpNotAllowed): + return jsonify(response.to_json()) + else: + return jsonify( + { + "status": response.status, + "reason": response.reason, + } + )