Skip to content

Commit 90846ef

Browse files
authored
Merge pull request #382 from supertokens/feat/dashboard-mt
feat: Add multitenancy for dashboard recipe
2 parents fee89fb + c7f87ac commit 90846ef

29 files changed

+221
-52
lines changed

supertokens_python/recipe/dashboard/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .users_count_get import handle_users_count_get_api
3232
from .users_get import handle_users_get_api
3333
from .validate_key import handle_validate_key_api
34+
from .list_tenants import handle_list_tenants_api
3435

3536
__all__ = [
3637
"handle_dashboard_api",
@@ -53,4 +54,5 @@
5354
"handle_emailpassword_signout_api",
5455
"handle_get_tags",
5556
"handle_analytics_post",
57+
"handle_list_tenants_api",
5658
]

supertokens_python/recipe/dashboard/api/analytics.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535

3636

3737
async def handle_analytics_post(
38-
_: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
38+
_: APIInterface,
39+
_tenant_id: str,
40+
api_options: APIOptions,
41+
_user_context: Dict[str, Any],
3942
) -> AnalyticsResponse:
4043
if not Supertokens.get_instance().telemetry:
4144
return AnalyticsResponse()

supertokens_python/recipe/dashboard/api/api_key_protector.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@
3232

3333
async def api_key_protector(
3434
api_implementation: APIInterface,
35+
tenant_id: str,
3536
api_options: APIOptions,
3637
api_function: Callable[
37-
[APIInterface, APIOptions, Dict[str, Any]], Awaitable[APIResponse]
38+
[APIInterface, str, APIOptions, Dict[str, Any]], Awaitable[APIResponse]
3839
],
3940
user_context: Dict[str, Any],
4041
) -> Optional[BaseResponse]:
@@ -47,5 +48,7 @@ async def api_key_protector(
4748
"Unauthorised access", 401, api_options.response
4849
)
4950

50-
response = await api_function(api_implementation, api_options, user_context)
51+
response = await api_function(
52+
api_implementation, tenant_id, api_options, user_context
53+
)
5154
return send_200_response(response.to_json(), api_options.response)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
2+
#
3+
# This software is licensed under the Apache License, Version 2.0 (the
4+
# "License") as published by the Apache Software Foundation.
5+
#
6+
# You may not use this file except in compliance with the License. You may
7+
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import TYPE_CHECKING, Any, Dict
18+
19+
if TYPE_CHECKING:
20+
from supertokens_python.recipe.dashboard.interfaces import (
21+
APIOptions,
22+
APIInterface,
23+
)
24+
from supertokens_python.types import APIResponse
25+
26+
from supertokens_python.recipe.multitenancy.asyncio import list_all_tenants
27+
from supertokens_python.recipe.dashboard.interfaces import (
28+
DashboardListTenantsGetResponse,
29+
)
30+
31+
32+
async def handle_list_tenants_api(
33+
_api_implementation: APIInterface,
34+
_tenant_id: str,
35+
_api_options: APIOptions,
36+
user_context: Dict[str, Any],
37+
) -> APIResponse:
38+
tenants = await list_all_tenants(user_context)
39+
return DashboardListTenantsGetResponse(tenants.tenants)

supertokens_python/recipe/dashboard/api/search/getTags.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
async def handle_get_tags(
27-
_: APIInterface, __: APIOptions, _user_context: Dict[str, Any]
27+
_: APIInterface, _tenant_id: str, __: APIOptions, _user_context: Dict[str, Any]
2828
) -> SearchTagsOK:
2929
response = await Querier.get_instance().send_get_request(
3030
NormalisedURLPath("/user/search/tags")

supertokens_python/recipe/dashboard/api/signout.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626

2727

2828
async def handle_emailpassword_signout_api(
29-
_: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
29+
_: APIInterface,
30+
_tenant_id: str,
31+
api_options: APIOptions,
32+
_user_context: Dict[str, Any],
3033
) -> SignOutOK:
3134
if api_options.config.auth_mode == "api-key":
3235
return SignOutOK()

supertokens_python/recipe/dashboard/api/userdetails/user_delete.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77

88
async def handle_user_delete(
9-
_api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
9+
_api_interface: APIInterface,
10+
_tenant_id: str,
11+
api_options: APIOptions,
12+
_user_context: Dict[str, Any],
1013
) -> UserDeleteAPIResponse:
1114
user_id = api_options.request.get_query_param("userId")
1215

supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_get.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212

1313

1414
async def handle_user_email_verify_get(
15-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
15+
_api_interface: APIInterface,
16+
_tenant_id: str,
17+
api_options: APIOptions,
18+
user_context: Dict[str, Any],
1619
) -> Union[UserEmailVerifyGetAPIResponse, FeatureNotEnabledError]:
1720
req = api_options.request
1821
user_id = req.get_query_param("userId")

supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_put.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020

2121
async def handle_user_email_verify_put(
22-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
22+
_api_interface: APIInterface,
23+
tenant_id: str,
24+
api_options: APIOptions,
25+
user_context: Dict[str, Any],
2326
) -> UserEmailVerifyPutAPIResponse:
2427
request_body: Dict[str, Any] = await api_options.request.json() # type: ignore
2528
user_id = request_body.get("userId")
@@ -37,7 +40,7 @@ async def handle_user_email_verify_put(
3740

3841
if verified:
3942
token_response = await create_email_verification_token(
40-
user_id, user_context=user_context
43+
user_id, tenant_id=tenant_id, user_context=user_context
4144
)
4245

4346
if isinstance(
@@ -46,7 +49,7 @@ async def handle_user_email_verify_put(
4649
return UserEmailVerifyPutAPIResponse()
4750

4851
verify_response = await verify_email_using_token(
49-
token_response.token, user_context=user_context
52+
token_response.token, tenant_id, user_context=user_context
5053
)
5154

5255
if isinstance(verify_response, VerifyEmailUsingTokenInvalidTokenError):

supertokens_python/recipe/dashboard/api/userdetails/user_email_verify_token_post.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828

2929

3030
async def handle_email_verify_token_post(
31-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
31+
_api_interface: APIInterface,
32+
tenant_id: str,
33+
api_options: APIOptions,
34+
user_context: Dict[str, Any],
3235
) -> Union[
3336
UserEmailVerifyTokenPostAPIOkResponse,
3437
UserEmailVerifyTokenPostAPIEmailAlreadyVerifiedErrorResponse,
@@ -49,7 +52,7 @@ async def handle_email_verify_token_post(
4952
raise Exception("Should not come here")
5053

5154
email_verification_token = await create_email_verification_token(
52-
user_id, user_context=user_context
55+
user_id, tenant_id=tenant_id, user_context=user_context
5356
)
5457

5558
if isinstance(
@@ -59,8 +62,6 @@ async def handle_email_verify_token_post(
5962

6063
assert isinstance(email_verification_token, CreateEmailVerificationTokenOkResult)
6164

62-
# TODO: Pass tenant id
63-
tenant_id = "pass-tenant-id"
6465
email_verify_link = get_email_verify_link(
6566
api_options.app_info, email_verification_token.token, user_id, tenant_id
6667
)

supertokens_python/recipe/dashboard/api/userdetails/user_get.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717

1818
async def handle_user_get(
19-
_api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
19+
_api_interface: APIInterface,
20+
_tenant_id: str,
21+
api_options: APIOptions,
22+
_user_context: Dict[str, Any],
2023
) -> Union[
2124
UserGetAPINoUserFoundError,
2225
UserGetAPIOkResponse,

supertokens_python/recipe/dashboard/api/userdetails/user_metadata_get.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212

1313
async def handle_metadata_get(
14-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
14+
_api_interface: APIInterface,
15+
_tenant_id: str,
16+
api_options: APIOptions,
17+
user_context: Dict[str, Any],
1518
) -> Union[UserMetadataGetAPIOkResponse, FeatureNotEnabledError]:
1619
user_id = api_options.request.get_query_param("userId")
1720

supertokens_python/recipe/dashboard/api/userdetails/user_metadata_put.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212

1313

1414
async def handle_metadata_put(
15-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
15+
_api_interface: APIInterface,
16+
_tenant_id: str,
17+
api_options: APIOptions,
18+
user_context: Dict[str, Any],
1619
) -> UserMetadataPutAPIResponse:
1720
request_body: Dict[str, Any] = await api_options.request.json() # type: ignore
1821
user_id = request_body.get("userId")

supertokens_python/recipe/dashboard/api/userdetails/user_password_put.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737

3838

3939
async def handle_user_password_put(
40-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
40+
_api_interface: APIInterface,
41+
tenant_id: str,
42+
api_options: APIOptions,
43+
user_context: Dict[str, Any],
4144
) -> Union[UserPasswordPutAPIResponse, UserPasswordPutAPIInvalidPasswordErrorResponse]:
4245
request_body: Dict[str, Any] = await api_options.request.json() # type: ignore
4346
user_id = request_body.get("userId")
@@ -100,9 +103,8 @@ async def reset_password(
100103
password_validation_error
101104
)
102105

103-
# TODO: Pass tenant id
104106
password_reset_token = await create_reset_password_token(
105-
"pass-tenant-id", user_id, user_context
107+
user_id, tenant_id, user_context
106108
)
107109

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

113115
password_reset_response = await reset_password_using_token(
114-
"pass-tenant-id", password_reset_token.token, new_password, user_context
116+
password_reset_token.token, new_password, tenant_id, user_context
115117
)
116118

117119
if isinstance(

supertokens_python/recipe/dashboard/api/userdetails/user_put.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ async def update_phone_for_recipe_id(
253253

254254

255255
async def handle_user_put(
256-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
256+
_api_interface: APIInterface,
257+
_tenant_id: str,
258+
api_options: APIOptions,
259+
user_context: Dict[str, Any],
257260
) -> Union[
258261
UserPutAPIOkResponse,
259262
UserPutAPIInvalidEmailErrorResponse,

supertokens_python/recipe/dashboard/api/userdetails/user_sessions_get.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717

1818
async def handle_sessions_get(
19-
_api_interface: APIInterface, api_options: APIOptions, user_context: Dict[str, Any]
19+
_api_interface: APIInterface,
20+
tenant_id: str,
21+
api_options: APIOptions,
22+
user_context: Dict[str, Any],
2023
) -> UserSessionsGetAPIResponse:
2124
user_id = api_options.request.get_query_param("userId")
2225

supertokens_python/recipe/dashboard/api/userdetails/user_sessions_post.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77

88
async def handle_user_sessions_post(
9-
_api_interface: APIInterface, api_options: APIOptions, _user_context: Dict[str, Any]
9+
_api_interface: APIInterface,
10+
_tenant_id: str,
11+
api_options: APIOptions,
12+
_user_context: Dict[str, Any],
1013
) -> UserSessionsPostAPIResponse:
1114
request_body = await api_options.request.json() # type: ignore
1215
session_handles: Optional[List[str]] = request_body.get("sessionHandles") # type: ignore

supertokens_python/recipe/dashboard/api/users_count_get.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626

2727

2828
async def handle_users_count_get_api(
29-
_: APIInterface, _api_options: APIOptions, _user_context: Dict[str, Any]
29+
_: APIInterface,
30+
tenant_id: str,
31+
_api_options: APIOptions,
32+
_user_context: Dict[str, Any],
3033
) -> UserCountGetAPIResponse:
31-
count = await Supertokens.get_instance().get_user_count(include_recipe_ids=None)
34+
count = await Supertokens.get_instance().get_user_count(None, tenant_id)
3235
return UserCountGetAPIResponse(count=count)

supertokens_python/recipe/dashboard/api/users_get.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
async def handle_users_get_api(
3737
api_implementation: APIInterface,
38+
tenant_id: str,
3839
api_options: APIOptions,
3940
user_context: Dict[str, Any],
4041
) -> APIResponse:
@@ -58,6 +59,7 @@ async def handle_users_get_api(
5859
pagination_token=pagination_token,
5960
include_recipe_ids=None,
6061
query=api_options.request.get_query_params(),
62+
tenant_id=tenant_id,
6163
)
6264

6365
# user metadata bulk fetch with batches:

supertokens_python/recipe/dashboard/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
EMAIL_PASSSWORD_SIGNOUT = "/api/signout"
1313
SEARCH_TAGS_API = "/api/search/tags"
1414
DASHBOARD_ANALYTICS_API = "/api/analytics"
15+
TENANTS_LIST_API = "/api/tenants/list"

supertokens_python/recipe/dashboard/interfaces.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ def to_json(self) -> Dict[str, Any]:
101101
}
102102

103103

104+
from supertokens_python.recipe.multitenancy.interfaces import ListAllTenantsOkResult
105+
106+
107+
class DashboardListTenantsGetResponse(APIResponse, ListAllTenantsOkResult):
108+
def to_json(self):
109+
return {
110+
"status": self.status,
111+
"tenants": [t.to_json() for t in self.tenants],
112+
}
113+
114+
104115
class UserCountGetAPIResponse(APIResponse):
105116
status: str = "OK"
106117

0 commit comments

Comments
 (0)