Skip to content

Commit

Permalink
Merge pull request #96 from supertokens/feat/multitenancy
Browse files Browse the repository at this point in the history
feat: Add basic multitenancy support to the dashboard
  • Loading branch information
rishabhpoddar authored Jul 18, 2023
2 parents 597fc60 + 6aa8796 commit 65b56e2
Show file tree
Hide file tree
Showing 39 changed files with 956 additions and 138 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [0.7.0] - 2023-07-18

- Adds the ability to choose a tenant when viewing the list of users
- User details now displays all the tenants associated with that user

## [0.6.7] - 2023-06-29

- Fixes an issue where trying to update a user's metadata would result in a screen error if the edited meta data was an invalid JSON. The error is now a local error for the meta data section in the case of failures.
Expand Down
40 changes: 40 additions & 0 deletions api_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ tags:
description: APIs for search
- name: "Telemetry"
description: APIs related to recording telemetry from the dashboard
- name: "Multitenancy"
description: APIs related to multitenancy

paths:
/signin:
Expand Down Expand Up @@ -1173,6 +1175,44 @@ paths:
items:
type: string

/tenants/list:
get:
tags:
- Multitenancy
summary: Get all tenants created in the core
responses:
200:
description: Success
content:
application/json:
schema:
type: object
properties:
status:
type: string
default: "OK"
coreConfig:
type: object
tenants:
type: array
items:
type: object
properties:
tenantId:
type: string
emailPassword:
type: object
properties:
enabled: boolean
passwordless:
type: object
properties:
enabled: boolean
thirdParty:
type: object
properties:
enabled: boolean

servers:
# Added by API Auto Mocking Plugin
- description: SwaggerHub API Auto Mocking
Expand Down
4 changes: 2 additions & 2 deletions build/static/css/main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/css/main.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dashboard",
"version": "0.6.7",
"version": "0.7.0",
"private": true,
"dependencies": {
"@babel/core": "^7.16.0",
Expand Down
19 changes: 14 additions & 5 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ app.use(morgan("[:date[iso]] :url :method :status :response-time ms - :res[conte
SuperTokens.init({
framework: "express",
supertokens: {
connectionURI: "https://try.supertokens.io",
connectionURI: "https://st-dev-fc045a40-2527-11ee-a35f-b5d577ffdb9c.aws.supertokens.io",
apiKey: "=XmrF3o7Qfqj7XJVomcvxGu=j6",
},
appInfo: {
appName: "Dashboard Dev Node",
Expand Down Expand Up @@ -66,10 +67,18 @@ SuperTokens.init({
ThirdParty.init({
signInAndUpFeature: {
providers: [
ThirdParty.Google({
clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com",
clientSecret: "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",
}),
{
config: {
thirdPartyId: "google",
clients: [
{
clientId:
"1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com",
clientSecret: "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",
},
],
},
},
],
},
}),
Expand Down
39 changes: 21 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* under the License.
*/

import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import UsersListPage from "./ui/pages/usersList/UsersList";
import { getDashboardAppBasePath } from "./utils";

Expand All @@ -26,30 +26,33 @@ import { LayoutModalContainer } from "./ui/components/layout/layoutModal";
import SafeAreaView from "./ui/components/safeAreaView/SafeAreaView";
import { ToastNotificationContainer } from "./ui/components/toast/toastNotification";
import { PopupContentContextProvider } from "./ui/contexts/PopupContentContext";
import { TenantsListContextProvider } from "./ui/contexts/TenantsListContext";

function App() {
return (
<>
<SafeAreaView />
<ErrorBoundary>
<PopupContentContextProvider>
<AuthWrapper>
<Router basename={getDashboardAppBasePath()}>
<SignOutBtn />
<Routes>
<Route
path="/"
element={<UsersListPage />}
/>
<Route
path="*"
element={<UsersListPage />}
/>
</Routes>
</Router>
<ToastNotificationContainer />
<LayoutModalContainer />
</AuthWrapper>
<TenantsListContextProvider>
<AuthWrapper>
<Router basename={getDashboardAppBasePath()}>
<SignOutBtn />
<Routes>
<Route
path="/"
element={<UsersListPage />}
/>
<Route
path="*"
element={<UsersListPage />}
/>
</Routes>
</Router>
<ToastNotificationContainer />
<LayoutModalContainer />
</AuthWrapper>
</TenantsListContextProvider>
</PopupContentContextProvider>
</ErrorBoundary>
</>
Expand Down
59 changes: 59 additions & 0 deletions src/api/tenants/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* Copyright (c) 2022, 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.
*/

import { getApiUrl, useFetchData } from "../../utils";

export type Tenant = {
tenantId: string;
emailPassword: {
enabled: boolean;
};
passwordless: {
enabled: boolean;
};
thirdParty: {
enabled: boolean;
};
};

type TenantsListResponse = {
status: "OK";
tenants: Tenant[];
};

type TenantsListService = {
fetchTenants: () => Promise<TenantsListResponse>;
};

export const useGetTenantsList = (): TenantsListService => {
const fetchData = useFetchData();
const fetchTenants = async (): Promise<TenantsListResponse> => {
const response = await fetchData({
method: "GET",
url: getApiUrl("/api/tenants/list"),
});

return response.ok
? await response.json()
: {
status: "OK",
tenants: [],
};
};

return {
fetchTenants,
};
};
14 changes: 11 additions & 3 deletions src/api/user/email/verify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { getApiUrl, useFetchData } from "../../../../utils";

interface IUseVerifyUserEmailService {
getUserEmailVerificationStatus: (userId: string) => Promise<EmailVerificationStatus>;
updateUserEmailVerificationStatus: (userId: string, isEmailVerified: boolean) => Promise<boolean>;
updateUserEmailVerificationStatus: (
userId: string,
isEmailVerified: boolean,
tenantId: string | undefined
) => Promise<boolean>;
}

const useVerifyUserEmail = (): IUseVerifyUserEmailService => {
Expand All @@ -20,9 +24,13 @@ const useVerifyUserEmail = (): IUseVerifyUserEmailService => {
return body;
};

const updateUserEmailVerificationStatus = async (userId: string, isEmailVerified: boolean) => {
const updateUserEmailVerificationStatus = async (
userId: string,
isEmailVerified: boolean,
tenantId: string | undefined
) => {
const response = await fetchData({
url: getApiUrl("/api/user/email/verify"),
url: getApiUrl("/api/user/email/verify", tenantId),
method: "PUT",
config: {
body: JSON.stringify({ verified: isEmailVerified, userId }),
Expand Down
6 changes: 3 additions & 3 deletions src/api/user/email/verify/token.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { getApiUrl, useFetchData } from "../../../../utils";

interface IUseVerifyUserTokenService {
sendUserEmailVerification: (userId: string) => Promise<boolean>;
sendUserEmailVerification: (userId: string, tenantId?: string) => Promise<boolean>;
}

const useVerifyUserTokenService = (): IUseVerifyUserTokenService => {
const fetchData = useFetchData();

const sendUserEmailVerification = async (userId: string) => {
const sendUserEmailVerification = async (userId: string, tenantId?: string) => {
const response = await fetchData({
url: getApiUrl("/api/user/email/verify/token"),
url: getApiUrl("/api/user/email/verify/token", tenantId),
method: "POST",
config: {
body: JSON.stringify({
Expand Down
4 changes: 3 additions & 1 deletion src/api/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface IUseUserService {
interface IUpdateUserInformationArgs {
userId: string;
recipeId: string;
tenantId: string | undefined;
email?: string;
phone?: string;
firstName?: string;
Expand Down Expand Up @@ -97,6 +98,7 @@ export const useUserService = (): IUseUserService => {
phone,
firstName,
lastName,
tenantId,
}: IUpdateUserInformationArgs): Promise<UpdateUserInformationResponse> => {
let emailToSend = email === undefined ? "" : email;
const phoneToSend = phone === undefined ? "" : phone;
Expand All @@ -108,7 +110,7 @@ export const useUserService = (): IUseUserService => {
}

const response = await fetchData({
url: getApiUrl("/api/user"),
url: getApiUrl("/api/user", tenantId),
method: "PUT",
config: {
body: JSON.stringify({
Expand Down
14 changes: 11 additions & 3 deletions src/api/user/password/reset/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { getApiUrl, useFetchData } from "../../../../utils";

interface IUsePasswordResetService {
updatePassword: (userId: string, newPassword: string) => Promise<UpdatePasswordResponse>;
updatePassword: (
userId: string,
newPassword: string,
tenantId: string | undefined
) => Promise<UpdatePasswordResponse>;
}

type UpdatePasswordResponse =
Expand All @@ -16,9 +20,13 @@ type UpdatePasswordResponse =
const usePasswordResetService = (): IUsePasswordResetService => {
const fetchData = useFetchData();

const updatePassword = async (userId: string, newPassword: string): Promise<UpdatePasswordResponse> => {
const updatePassword = async (
userId: string,
newPassword: string,
tenantId: string | undefined
): Promise<UpdatePasswordResponse> => {
const response = await fetchData({
url: getApiUrl("/api/user/password"),
url: getApiUrl("/api/user/password", tenantId),
method: "PUT",
query: { userId },
config: {
Expand Down
6 changes: 3 additions & 3 deletions src/api/users/count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import { UserListCount } from "../../ui/pages/usersList/types";
import { getApiUrl, useFetchData } from "../../utils";

interface IUseFetchCountService {
fetchCount: () => Promise<UserListCount | undefined>;
fetchCount: (tenantid?: string) => Promise<UserListCount | undefined>;
}

const useFetchCountService = (): IUseFetchCountService => {
const fetchData = useFetchData();

const fetchCount = async () => {
const fetchCount = async (tenantId?: string) => {
const response = await fetchData({
url: getApiUrl("/api/users/count"),
url: getApiUrl("/api/users/count", tenantId),
method: "GET",
});

Expand Down
11 changes: 8 additions & 3 deletions src/api/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ import { getApiUrl, useFetchData } from "../../utils";
interface IUseFetchUsersService {
fetchUsers: (
param?: { paginationToken?: string; limit?: number },
search?: object
search?: object,
tenantId?: string
) => Promise<UserPaginationList | undefined>;
}

export const useFetchUsersService = (): IUseFetchUsersService => {
const fetchData = useFetchData();
const fetchUsers = async (param?: { paginationToken?: string; limit?: number }, search?: object) => {
const fetchUsers = async (
param?: { paginationToken?: string; limit?: number },
search?: object,
tenantId?: string
) => {
let query = {};
if (search) {
query = { ...search };
Expand All @@ -40,7 +45,7 @@ export const useFetchUsersService = (): IUseFetchUsersService => {
query = { ...query, limit: LIST_DEFAULT_LIMIT };
}
const response = await fetchData({
url: getApiUrl("/api/users"),
url: getApiUrl("/api/users", tenantId),
method: "GET",
query: query,
});
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
export class StorageKeys {
static AUTH_KEY = "auth-token";
static EMAIL = "email";
static TENANT_ID = "tenant-id";
}

// Add types as required
Expand Down
Loading

0 comments on commit 65b56e2

Please sign in to comment.