Skip to content
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

feat: Add basic multitenancy support to the dashboard #96

Merged
merged 14 commits into from
Jul 18, 2023
Merged
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
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
Loading