Skip to content

feat: Add User ID mapping recipe #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
70 changes: 70 additions & 0 deletions supertokens_python/recipe/useridmapping/asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 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 typing import Union, Dict, Any, Optional

from supertokens_python.recipe.useridmapping.interfaces import (
UserIdMappingAlreadyExistsError,
UnknownMappingError,
UpdateOrDeleteUserIdMappingInfoOkResult,
UserIDTypes,
CreateUserIdMappingOkResult,
UnknownSupertokensUserIDError,
GetUserIdMappingOkResult,
DeleteUserIdMappingOkResult,
)

from supertokens_python.recipe.useridmapping.recipe import UserIdMappingRecipe


async def create_user_id_mapping(
supertokens_user_id: str,
external_user_id: str,
external_user_id_info: Optional[str],
user_context: Dict[str, Any],
) -> Union[
CreateUserIdMappingOkResult,
UnknownSupertokensUserIDError,
UserIdMappingAlreadyExistsError,
]:
return await UserIdMappingRecipe.get_instance().recipe_implementation.create_user_id_mapping(
supertokens_user_id, external_user_id, external_user_id_info, user_context
)


async def get_user_id_mapping(
user_id: str, user_id_type: UserIDTypes, user_context: Dict[str, Any]
) -> Union[GetUserIdMappingOkResult, UnknownMappingError]:
return await UserIdMappingRecipe.get_instance().recipe_implementation.get_user_id_mapping(
user_id, user_id_type, user_context
)


async def delete_user_id_mapping(
user_id: str, user_id_type: UserIDTypes, user_context: Dict[str, Any]
) -> Union[DeleteUserIdMappingOkResult, UnknownMappingError]:
return await UserIdMappingRecipe.get_instance().recipe_implementation.delete_user_id_mapping(
user_id, user_id_type, user_context
)


async def update_or_delete_user_id_mapping_info(
user_id: str,
user_id_type: UserIDTypes,
external_user_id_info: Optional[str],
user_context: Dict[str, Any],
) -> Union[UpdateOrDeleteUserIdMappingInfoOkResult, UnknownMappingError]:
return await UserIdMappingRecipe.get_instance().recipe_implementation.update_or_delete_user_id_mapping_info(
user_id, user_id_type, external_user_id_info, user_context
)
5 changes: 5 additions & 0 deletions supertokens_python/recipe/useridmapping/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from supertokens_python.exceptions import SuperTokensError


class SuperTokensUserIdMappingError(SuperTokensError):
pass
89 changes: 89 additions & 0 deletions supertokens_python/recipe/useridmapping/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, Union, Optional
from typing_extensions import Literal


class UnknownSupertokensUserIDError:
pass


class CreateUserIdMappingOkResult:
pass


class UserIdMappingAlreadyExistsError:
def __init__(
self, does_super_tokens_user_id_exist: bool, does_external_user_id_exist: str
):
self.does_super_tokens_user_id_exist = does_super_tokens_user_id_exist
self.does_external_user_id_exist = does_external_user_id_exist


UserIDTypes = Literal["SUPERTOKENS", "EXTERNAL", "ANY"]


class GetUserIdMappingOkResult:
def __init__(
self,
supertokens_user_id: str,
external_user_id: str,
external_user_info: Optional[str],
):
self.supertokens_user_id = supertokens_user_id
self.external_user_id = external_user_id
self.external_user_info = external_user_info


class UnknownMappingError:
pass


class DeleteUserIdMappingOkResult:
def __init__(self, did_mapping_exist: bool):
self.did_mapping_exist = did_mapping_exist


class UpdateOrDeleteUserIdMappingInfoOkResult:
pass


class RecipeInterface(ABC):
@abstractmethod
async def create_user_id_mapping(
self,
supertokens_user_id: str,
external_user_id: str,
external_user_id_info: Optional[str],
user_context: Dict[str, Any],
) -> Union[
CreateUserIdMappingOkResult,
UnknownSupertokensUserIDError,
UserIdMappingAlreadyExistsError,
]:
pass

@abstractmethod
async def get_user_id_mapping(
self, user_id: str, user_id_type: UserIDTypes, user_context: Dict[str, Any]
) -> Union[GetUserIdMappingOkResult, UnknownMappingError]:
pass

@abstractmethod
async def delete_user_id_mapping(
self, user_id: str, user_id_type: UserIDTypes, user_context: Dict[str, Any]
) -> DeleteUserIdMappingOkResult:
pass

@abstractmethod
async def update_or_delete_user_id_mapping_info(
self,
user_id: str,
user_id_type: UserIDTypes,
external_user_id_info: Optional[str],
user_context: Dict[str, Any],
) -> Union[UpdateOrDeleteUserIdMappingInfoOkResult, UnknownMappingError]:
pass


class APIInterface(ABC):
pass
111 changes: 111 additions & 0 deletions supertokens_python/recipe/useridmapping/recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# 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 os import environ
from typing import List, Union

from supertokens_python.exceptions import SuperTokensError, raise_general_exception
from supertokens_python.framework import BaseRequest, BaseResponse
from supertokens_python.normalised_url_path import NormalisedURLPath
from supertokens_python.querier import Querier
from supertokens_python.recipe.useridmapping.recipe_implementation import (
RecipeImplementation,
)
from supertokens_python.recipe.useridmapping.utils import (
validate_and_normalise_user_input,
)
from supertokens_python.recipe_module import APIHandled, RecipeModule
from supertokens_python.supertokens import AppInfo

from .exceptions import SuperTokensUserIdMappingError
from .utils import InputOverrideConfig


class UserIdMappingRecipe(RecipeModule):
recipe_id = "useridmapping"
__instance = None

def __init__(
self,
recipe_id: str,
app_info: AppInfo,
override: Union[InputOverrideConfig, None] = None,
):
super().__init__(recipe_id, app_info)
self.config = validate_and_normalise_user_input(self, app_info, override)
recipe_implementation = RecipeImplementation(Querier.get_instance(recipe_id))
self.recipe_implementation = (
recipe_implementation
if self.config.override.functions is None
else self.config.override.functions(recipe_implementation)
)

def is_error_from_this_recipe_based_on_instance(self, err: Exception) -> bool:
return isinstance(err, SuperTokensError) and (
isinstance(err, SuperTokensUserIdMappingError)
)

def get_apis_handled(self) -> List[APIHandled]:
return []

async def handle_api_request(
self,
request_id: str,
request: BaseRequest,
path: NormalisedURLPath,
method: str,
response: BaseResponse,
) -> Union[BaseResponse, None]:
raise Exception("Should never come here")

async def handle_error(
self, request: BaseRequest, err: SuperTokensError, response: BaseResponse
) -> BaseResponse:
raise err

def get_all_cors_headers(self) -> List[str]:
return []

@staticmethod
def init(override: Union[InputOverrideConfig, None] = None):
def func(app_info: AppInfo):
if UserIdMappingRecipe.__instance is None:
UserIdMappingRecipe.__instance = UserIdMappingRecipe(
UserIdMappingRecipe.recipe_id, app_info, override
)
return UserIdMappingRecipe.__instance
raise Exception(
None,
"UserIdMapping recipe has already been initialised. Please check your code for bugs.",
)

return func

@staticmethod
def reset():
if ("SUPERTOKENS_ENV" not in environ) or (
environ["SUPERTOKENS_ENV"] != "testing"
):
raise_general_exception("calling testing function in non testing env")
UserIdMappingRecipe.__instance = None

@staticmethod
def get_instance() -> UserIdMappingRecipe:
if UserIdMappingRecipe.__instance is not None:
return UserIdMappingRecipe.__instance
raise_general_exception(
"Initialisation not done. Did you forget to call the SuperTokens.init or UserIdMapping.init function?"
)
98 changes: 98 additions & 0 deletions supertokens_python/recipe/useridmapping/recipe_implementation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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 typing import Any, Dict, Union, Optional

from supertokens_python.normalised_url_path import NormalisedURLPath
from supertokens_python.querier import Querier
from .interfaces import (
RecipeInterface,
UnknownMappingError,
UnknownSupertokensUserIDError,
DeleteUserIdMappingOkResult,
UserIDTypes,
UpdateOrDeleteUserIdMappingInfoOkResult,
GetUserIdMappingOkResult,
UserIdMappingAlreadyExistsError,
CreateUserIdMappingOkResult,
)


class RecipeImplementation(RecipeInterface):
def __init__(self, querier: Querier):
super().__init__()
self.querier = querier

async def create_user_id_mapping(
self,
supertokens_user_id: str,
external_user_id: str,
external_user_id_info: Optional[str],
user_context: Dict[str, Any],
) -> Union[
CreateUserIdMappingOkResult,
UnknownSupertokensUserIDError,
UserIdMappingAlreadyExistsError,
]:
return await self.querier.send_post_request(
NormalisedURLPath("/recipe/userid/map"),
{
"supertokensUserId": supertokens_user_id,
"externalUserId": external_user_id,
"externalUserIdInfo": external_user_id_info,
},
)

async def get_user_id_mapping(
self, user_id: str, user_id_type: UserIDTypes, user_context: Dict[str, Any]
) -> Union[GetUserIdMappingOkResult, UnknownMappingError]:
if user_context.get("_default", {}).get("userIdMapping") is not None:
return user_context["_default"]["userIdMapping"]

response = await self.querier.send_get_request(
NormalisedURLPath("/recipe/userid/map"),
{"userId": user_id, "userIdType": user_id_type},
)

user_context["_default"] = {
**user_context["_default"],
"userIdMapping": response,
}

return response

async def delete_user_id_mapping(
self, user_id: str, user_id_type: UserIDTypes, user_context: Dict[str, Any]
) -> DeleteUserIdMappingOkResult:
return await self.querier.send_post_request(
NormalisedURLPath("/recipe/userid/map/remove"),
{"userId": user_id, "userIdType": user_id_type},
)

async def update_or_delete_user_id_mapping_info(
self,
user_id: str,
user_id_type: UserIDTypes,
external_user_id_info: Optional[str],
user_context: Dict[str, Any],
) -> Union[UpdateOrDeleteUserIdMappingInfoOkResult, UnknownMappingError]:
return await self.querier.send_put_request(
NormalisedURLPath("/recipe/userid/map/info"),
{
"userId": user_id,
"userIdType": user_id_type,
"externalUserIdInfo": external_user_id_info,
},
)
Loading