Skip to content

Commit

Permalink
feat: Add multitenancy for dashboard recipe
Browse files Browse the repository at this point in the history
  • Loading branch information
KShivendu committed Jul 27, 2023
1 parent bff0cbd commit 9c49016
Show file tree
Hide file tree
Showing 37 changed files with 301 additions and 89 deletions.
2 changes: 2 additions & 0 deletions supertokens_python/recipe/dashboard/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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
from .list_tenants import handle_list_tenants_api

__all__ = [
"handle_dashboard_api",
Expand All @@ -53,4 +54,5 @@
"handle_emailpassword_signout_api",
"handle_get_tags",
"handle_analytics_post",
"handle_list_tenants_api",
]
5 changes: 4 additions & 1 deletion supertokens_python/recipe/dashboard/api/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@


async def handle_analytics_post(
_: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
_: APIInterface,
_tenant_id: str,
api_options: APIOptions,
_user_context: Dict[str, Any],
) -> AnalyticsResponse:
if not Supertokens.get_instance().telemetry:
return AnalyticsResponse()
Expand Down
7 changes: 5 additions & 2 deletions supertokens_python/recipe/dashboard/api/api_key_protector.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@

async def api_key_protector(
api_implementation: APIInterface,
tenant_id: str,
api_options: APIOptions,
api_function: Callable[
[APIInterface, APIOptions, Dict[str, Any]], Awaitable[APIResponse]
[APIInterface, str, APIOptions, Dict[str, Any]], Awaitable[APIResponse]
],
user_context: Dict[str, Any],
) -> Optional[BaseResponse]:
Expand All @@ -47,5 +48,7 @@ async def api_key_protector(
"Unauthorised access", 401, api_options.response
)

response = await api_function(api_implementation, api_options, user_context)
response = await api_function(
api_implementation, tenant_id, api_options, user_context
)
return send_200_response(response.to_json(), api_options.response)
39 changes: 39 additions & 0 deletions supertokens_python/recipe/dashboard/api/list_tenants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) 2023, 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

if TYPE_CHECKING:
from supertokens_python.recipe.dashboard.interfaces import (
APIOptions,
APIInterface,
)
from supertokens_python.types import APIResponse

from supertokens_python.recipe.multitenancy.asyncio import list_all_tenants
from supertokens_python.recipe.dashboard.interfaces import (
DashboardListTenantsGetResponse,
)


async def handle_list_tenants_api(
_api_implementation: APIInterface,
_tenant_id: str,
_api_options: APIOptions,
user_context: Dict[str, Any],
) -> APIResponse:
tenants = await list_all_tenants(user_context)
return DashboardListTenantsGetResponse(tenants.tenants)
2 changes: 1 addition & 1 deletion supertokens_python/recipe/dashboard/api/search/getTags.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


async def handle_get_tags(
_: APIInterface, __: APIOptions, _user_context: Dict[str, Any]
_: APIInterface, _tenant_id: str, __: APIOptions, _user_context: Dict[str, Any]
) -> SearchTagsOK:
response = await Querier.get_instance().send_get_request(
NormalisedURLPath("/user/search/tags")
Expand Down
5 changes: 4 additions & 1 deletion supertokens_python/recipe/dashboard/api/signout.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@


async def handle_emailpassword_signout_api(
_: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
_: APIInterface,
_tenant_id: str,
api_options: APIOptions,
_user_context: Dict[str, Any],
) -> SignOutOK:
if api_options.config.auth_mode == "api-key":
return SignOutOK()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@


async def handle_user_delete(
_api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
_user_context: Dict[str, Any],
) -> UserDeleteAPIResponse:
user_id = api_options.request.get_query_param("userId")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@


async def handle_user_email_verify_get(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> Union[UserEmailVerifyGetAPIResponse, FeatureNotEnabledError]:
req = api_options.request
user_id = req.get_query_param("userId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@


async def handle_user_email_verify_put(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> UserEmailVerifyPutAPIResponse:
request_body: Dict[str, Any] = await api_options.request.json() # type: ignore
user_id = request_body.get("userId")
Expand All @@ -37,7 +40,7 @@ async def handle_user_email_verify_put(

if verified:
token_response = await create_email_verification_token(
user_id, user_context=user_context
user_id, tenant_id=tenant_id, user_context=user_context
)

if isinstance(
Expand All @@ -46,7 +49,7 @@ async def handle_user_email_verify_put(
return UserEmailVerifyPutAPIResponse()

verify_response = await verify_email_using_token(
token_response.token, user_context=user_context
token_response.token, tenant_id, user_context=user_context
)

if isinstance(verify_response, VerifyEmailUsingTokenInvalidTokenError):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@


async def handle_email_verify_token_post(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> Union[
UserEmailVerifyTokenPostAPIOkResponse,
UserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse,
Expand All @@ -49,7 +52,7 @@ async def handle_email_verify_token_post(
raise Exception("Should not come here")

email_verification_token = await create_email_verification_token(
user_id, user_context=user_context
user_id, tenant_id=tenant_id, user_context=user_context
)

if isinstance(
Expand All @@ -59,8 +62,6 @@ async def handle_email_verify_token_post(

assert isinstance(email_verification_token, CreateEmailVerificationTokenOkResult)

# TODO: Pass tenant id
tenant_id = "pass-tenant-id"
email_verify_link = get_email_verify_link(
api_options.app_info, email_verification_token.token, user_id, tenant_id
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@


async def handle_user_get(
_api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
_user_context: Dict[str, Any],
) -> Union[
UserGetAPINoUserFoundError,
UserGetAPIOkResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@


async def handle_metadata_get(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> Union[UserMetadataGetAPIOkResponse, FeatureNotEnabledError]:
user_id = api_options.request.get_query_param("userId")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@


async def handle_metadata_put(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> UserMetadataPutAPIResponse:
request_body: Dict[str, Any] = await api_options.request.json() # type: ignore
user_id = request_body.get("userId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@


async def handle_user_password_put(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> Union[UserPasswordPutAPIResponse, UserPasswordPutAPIInvalidPasswordErrorResponse]:
request_body: Dict[str, Any] = await api_options.request.json() # type: ignore
user_id = request_body.get("userId")
Expand Down Expand Up @@ -100,9 +103,8 @@ async def reset_password(
password_validation_error
)

# TODO: Pass tenant id
password_reset_token = await create_reset_password_token(
"pass-tenant-id", user_id, user_context
user_id, tenant_id, user_context
)

if isinstance(password_reset_token, CreateResetPasswordWrongUserIdError):
Expand All @@ -111,7 +113,7 @@ async def reset_password(
raise Exception("Should never come here")

password_reset_response = await reset_password_using_token(
"pass-tenant-id", password_reset_token.token, new_password, user_context
password_reset_token.token, new_password, tenant_id, user_context
)

if isinstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ async def update_phone_for_recipe_id(


async def handle_user_put(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> Union[
UserPutAPIOkResponse,
UserPutAPIInvalidEmailErrorResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@


async def handle_sessions_get(
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> 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, user_context)
session_handles = await get_all_session_handles_for_user(
user_id, None, user_context
)
sessions: List[Optional[SessionInfo]] = [None for _ in session_handles]

async def call_(i: int, session_handle: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@


async def handle_user_sessions_post(
_api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
_api_interface: APIInterface,
_tenant_id: str,
api_options: APIOptions,
_user_context: Dict[str, Any],
) -> UserSessionsPostAPIResponse:
request_body = await api_options.request.json() # type: ignore
session_handles: Optional[List[str]] = request_body.get("sessionHandles") # type: ignore
Expand Down
7 changes: 5 additions & 2 deletions supertokens_python/recipe/dashboard/api/users_count_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@


async def handle_users_count_get_api(
_: APIInterface, _api_options: APIOptions, _user_context: Dict[str, Any]
_: APIInterface,
tenant_id: str,
_api_options: APIOptions,
_user_context: Dict[str, Any],
) -> UserCountGetAPIResponse:
count = await Supertokens.get_instance().get_user_count(include_recipe_ids=None)
count = await Supertokens.get_instance().get_user_count(None, tenant_id)
return UserCountGetAPIResponse(count=count)
2 changes: 2 additions & 0 deletions supertokens_python/recipe/dashboard/api/users_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

async def handle_users_get_api(
api_implementation: APIInterface,
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
) -> APIResponse:
Expand All @@ -58,6 +59,7 @@ async def handle_users_get_api(
pagination_token=pagination_token,
include_recipe_ids=None,
query=api_options.request.get_query_params(),
tenant_id=tenant_id,
)

# user metadata bulk fetch with batches:
Expand Down
1 change: 1 addition & 0 deletions supertokens_python/recipe/dashboard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
EMAIL_PASSSWORD_SIGNOUT = "/api/signout"
SEARCH_TAGS_API = "/api/search/tags"
DASHBOARD_ANALYTICS_API = "/api/analytics"
TENANTS_LIST_API = "/api/tenants/list"
11 changes: 11 additions & 0 deletions supertokens_python/recipe/dashboard/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ def to_json(self) -> Dict[str, Any]:
}


from supertokens_python.recipe.multitenancy.interfaces import ListAllTenantsOkResult


class DashboardListTenantsGetResponse(APIResponse, ListAllTenantsOkResult):
def to_json(self):
return {
"status": self.status,
"tenants": [t.to_json() for t in self.tenants],
}


class UserCountGetAPIResponse(APIResponse):
status: str = "OK"

Expand Down
Loading

0 comments on commit 9c49016

Please sign in to comment.