Skip to content

Commit

Permalink
Handle no login methods state
Browse files Browse the repository at this point in the history
  • Loading branch information
prateek3255 committed Mar 14, 2024
1 parent 024d90a commit b4ffa8c
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 105 deletions.
20 changes: 0 additions & 20 deletions src/api/tenants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,25 +166,6 @@ export const useTenantService = () => {
export const useThirdPartyService = () => {
const fetchData = useFetchData();

const getThirdPartyProviders = async (
tenantId: string
): Promise<{
status: "OK";
providers: ProviderConfig[];
}> => {
const response = await fetchData({
url: getApiUrl(`/api/tenants/third-party/providers?tenantId=${tenantId}`),
method: "GET",
});

if (response.ok) {
const body = await response.json();
return body;
}

throw new Error("Unknown error");
};

const createOrUpdateThirdPartyProvider = async (tenantId: string, providerConfig: ProviderConfig) => {
const response = await fetchData({
url: getApiUrl("/api/tenants/third-party"),
Expand Down Expand Up @@ -223,6 +204,5 @@ export const useThirdPartyService = () => {
return {
createOrUpdateThirdPartyProvider,
deleteThirdPartyProvider,
getThirdPartyProviders,
};
};
2 changes: 2 additions & 0 deletions src/api/tenants/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export type Tenant = {
thirdParty: {
enabled: boolean;
};
firstFactors?: Array<string>;
requiredSecondaryFactors?: Array<string>;
};

type TenantsListResponse = {
Expand Down
4 changes: 3 additions & 1 deletion src/api/tenants/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type TenantInfo = {
};
thirdParty: {
enabled: boolean;
providers: ProviderConfig[];
providers: Array<ProviderConfig>;
};
passwordless: {
enabled: boolean;
Expand All @@ -56,6 +56,8 @@ export type TenantInfo = {
requiredSecondaryFactors?: Array<string>;
coreConfig: Record<string, unknown>;
userCount: number;
validFirstFactors: Array<string>;
mergedProvidersFromCoreAndStatic: Array<ProviderConfig>;
};

export type UpdateTenant = {
Expand Down
62 changes: 62 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,65 @@ export const IN_BUILT_THIRD_PARTY_PROVIDERS = [
icon: "provider-gitlab.svg",
},
];

export const FIRST_FACTOR_IDS = [
{
label: "Email Password",
description: "Sign in/up using email and password (Requires the EmailPassword recipe to be enabled)",
id: "emailpassword",
loginMethod: "emailpassword",
},
{
label: "OTP - Email",
description: "Sign in/up using OTP sent to email (Requires the Passwordless recipe to be enabled)",
id: "otp-email",
loginMethod: "passwordless",
},
{
label: "OTP - Phone",
description: "Sign in/up using OTP sent to phone (Requires the Passwordless recipe to be enabled)",
id: "otp-phone",
loginMethod: "passwordless",
},
{
label: "Link - Email",
description: "Sign in/up using link sent to email (Requires the Passwordless recipe to be enabled)",
id: "link-email",
loginMethod: "passwordless",
},
{
label: "Link - Phone",
description: "Sign in/up using link sent to phone (Requires the Passwordless recipe to be enabled)",
id: "link-phone",
loginMethod: "passwordless",
},
{
label: "Third Party",
description: "Sign in/up using third party providers (Requires the ThirdParty recipe to be enabled)",
id: "thirdparty",
loginMethod: "thirdparty",
},
];

export const SECONDARY_FACTOR_IDS = [
{
label: "TOTP",
description:
"Require TOTP as a secondary factor for successful authentication (Requires the TOTP recipe to be enabled)",
id: "totp",
},
{
label: "OTP - Email",
description:
"Require OTP sent to email as a secondary factor for successful authentication (Requires the Passwordless recipe to be enabled)",
id: "otp-email",
loginMethod: "passwordless",
},
{
label: "OTP - Phone",
description:
"Require OTP sent to phone as a secondary factor for successful authentication (Requires the Passwordless recipe to be enabled)",
id: "otp-phone",
loginMethod: "passwordless",
},
];
63 changes: 1 addition & 62 deletions src/ui/components/tenants/tenantDetail/LoginMethodsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,75 +16,14 @@ import { useCallback, useContext, useState } from "react";
import { useTenantService } from "../../../../api/tenants";
import { TenantInfo } from "../../../../api/tenants/types";
import { ReactComponent as InfoIcon } from "../../../../assets/info-icon.svg";
import { FIRST_FACTOR_IDS, SECONDARY_FACTOR_IDS } from "../../../../constants";
import { debounce, getImageUrl } from "../../../../utils";
import { PopupContentContext } from "../../../contexts/PopupContentContext";
import { Toggle } from "../../toggle/Toggle";
import TooltipContainer from "../../tooltip/tooltip";
import { useTenantDetailContext } from "./TenantDetailContext";
import { PanelHeader, PanelHeaderTitleWithTooltip, PanelRoot } from "./tenantDetailPanel/TenantDetailPanel";

const FIRST_FACTOR_IDS = [
{
label: "Email Password",
description: "Sign in/up using email and password (Requires the EmailPassword recipe to be enabled)",
id: "emailpassword",
loginMethod: "emailpassword",
},
{
label: "OTP - Email",
description: "Sign in/up using OTP sent to email (Requires the Passwordless recipe to be enabled)",
id: "otp-email",
loginMethod: "passwordless",
},
{
label: "OTP - Phone",
description: "Sign in/up using OTP sent to phone (Requires the Passwordless recipe to be enabled)",
id: "otp-phone",
loginMethod: "passwordless",
},
{
label: "Link - Email",
description: "Sign in/up using link sent to email (Requires the Passwordless recipe to be enabled)",
id: "link-email",
loginMethod: "passwordless",
},
{
label: "Link - Phone",
description: "Sign in/up using link sent to phone (Requires the Passwordless recipe to be enabled)",
id: "link-phone",
loginMethod: "passwordless",
},
{
label: "Third Party",
description: "Sign in/up using third party providers (Requires the ThirdParty recipe to be enabled)",
id: "thirdparty",
loginMethod: "thirdparty",
},
];

const SECONDARY_FACTOR_IDS = [
{
label: "TOTP",
description:
"Require TOTP as a secondary factor for successful authentication (Requires the TOTP recipe to be enabled)",
id: "totp",
},
{
label: "OTP - Email",
description:
"Require OTP sent to email as a secondary factor for successful authentication (Requires the Passwordless recipe to be enabled)",
id: "otp-email",
loginMethod: "passwordless",
},
{
label: "OTP - Phone",
description:
"Require OTP sent to phone as a secondary factor for successful authentication (Requires the Passwordless recipe to be enabled)",
id: "otp-phone",
loginMethod: "passwordless",
},
];

const getFirstFactorIds = (tenant: TenantInfo) => {
if (!tenant.firstFactors) {
const firstFactors = [];
Expand Down
24 changes: 8 additions & 16 deletions src/ui/components/tenants/tenantDetail/TenantDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
* under the License.
*/
import { useEffect, useState } from "react";
import { useCoreConfigService, useTenantService, useThirdPartyService } from "../../../../api/tenants";
import { CoreConfigOptions, ProviderConfig, TenantDashboardView, TenantInfo } from "../../../../api/tenants/types";
import { useCoreConfigService, useTenantService } from "../../../../api/tenants";
import { CoreConfigOptions, TenantDashboardView, TenantInfo } from "../../../../api/tenants/types";
import { ReactComponent as NoTenantFound } from "../../../../assets/no-tenants.svg";
import { PUBLIC_TENANT_ID } from "../../../../constants";
import { getImageUrl } from "../../../../utils";
Expand All @@ -38,10 +38,8 @@ export const TenantDetail = ({
}) => {
const { getTenantInfo } = useTenantService();
const { getCoreConfigOptions } = useCoreConfigService();
const { getThirdPartyProviders } = useThirdPartyService();
const [tenant, setTenant] = useState<TenantInfo | undefined>(undefined);
const [configOptions, setConfigOptions] = useState<CoreConfigOptions>([]);
const [resolvedProviders, setResolvedProviders] = useState<Array<ProviderConfig>>([]);
const [isLoading, setIsLoading] = useState(false);
const [isDeleteTenantDialogOpen, setIsDeleteTenantDialogOpen] = useState(false);
const [showLoadingOverlay, setShowLoadingOverlay] = useState(false);
Expand All @@ -65,20 +63,12 @@ export const TenantDetail = ({
}
};

const getThirdPartyProvidersInfo = async () => {
const repsonse = await getThirdPartyProviders(tenantId);
if (repsonse?.status === "OK") {
setResolvedProviders(repsonse.providers);
}
};

useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
await getTenant();
await getCoreConfig();
await getThirdPartyProvidersInfo();
} catch (_) {
} finally {
setIsLoading(false);
Expand All @@ -90,7 +80,6 @@ export const TenantDetail = ({
const refetchTenant = async () => {
setShowLoadingOverlay(true);
await getTenant();
await getThirdPartyProvidersInfo();
setShowLoadingOverlay(false);
};

Expand Down Expand Up @@ -118,6 +107,10 @@ export const TenantDetail = ({
};

const renderView = () => {
if (!tenant) {
throw new Error("This should be unreachable");
}

if (viewObj.view === "list-third-party-providers" || viewObj.view === "add-or-edit-third-party-provider") {
return (
<ThirdPartyPage
Expand Down Expand Up @@ -163,7 +156,7 @@ export const TenantDetail = ({
{isDeleteTenantDialogOpen && (
<DeleteTenantDialog
onCloseDialog={() => setIsDeleteTenantDialogOpen(false)}
tenantId={tenant!.tenantId}
tenantId={tenant.tenantId}
goBack={onBackButtonClicked}
/>
)}
Expand Down Expand Up @@ -194,8 +187,7 @@ export const TenantDetail = ({
tenantInfo={tenant}
setTenantInfo={setTenant}
coreConfigOptions={configOptions}
refetchTenant={refetchTenant}
resolvedProviders={resolvedProviders}>
refetchTenant={refetchTenant}>
{renderView()}
</TenantDetailContextProvider>
);
Expand Down
10 changes: 7 additions & 3 deletions src/ui/components/tenants/tenantDetail/TenantDetailContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,23 @@ export const TenantDetailContextProvider = ({
tenantInfo,
coreConfigOptions,
refetchTenant,
resolvedProviders,
setTenantInfo,
}: {
children: React.ReactNode;
tenantInfo: TenantInfo;
coreConfigOptions: CoreConfigOptions;
refetchTenant: () => Promise<void>;
resolvedProviders: Array<ProviderConfig>;
setTenantInfo: Dispatch<SetStateAction<TenantInfo | undefined>>;
}) => {
return (
<TenantDetailContext.Provider
value={{ tenantInfo, refetchTenant, coreConfigOptions, setTenantInfo, resolvedProviders }}>
value={{
tenantInfo,
refetchTenant,
coreConfigOptions,
setTenantInfo,
resolvedProviders: tenantInfo.mergedProvidersFromCoreAndStatic,
}}>
{children}
</TenantDetailContext.Provider>
);
Expand Down
55 changes: 52 additions & 3 deletions src/ui/components/tenants/tenantsListTable/TenantsListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* under the License.
*/
import { Tenant } from "../../../../api/tenants/list";
import { FIRST_FACTOR_IDS } from "../../../../constants";
import { getImageUrl, getInitializedRecipes } from "../../../../utils";
import Pagination from "../../pagination";
import { RecipePill } from "../../recipePill/RecipePill";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../table";
Expand All @@ -31,21 +33,68 @@ type TenantsListTableProps = {
};

const TenantLoginMethods = ({ tenant }: { tenant: Tenant }) => {
const getEnabledLoginMethods = () => {
const tenantLoginMethods = {
emailPassword: tenant.emailPassword.enabled,
passwordless: tenant.passwordless.enabled,
thirdParty: tenant.thirdParty.enabled,
};
if (Array.isArray(tenant.firstFactors) && tenant.firstFactors.length > 0) {
const allFactors = Array.from(
new Set([...tenant.firstFactors, ...(tenant.requiredSecondaryFactors ?? [])])
);
tenantLoginMethods.emailPassword = allFactors.some((factor) =>
FIRST_FACTOR_IDS.some((f) => f.loginMethod === "emailpassword" && f.id === factor)
);
tenantLoginMethods.passwordless = allFactors.some((factor) =>
FIRST_FACTOR_IDS.some((f) => f.loginMethod === "otp-email" && f.id === factor)
);
tenantLoginMethods.thirdParty = allFactors.some((factor) =>
FIRST_FACTOR_IDS.some((f) => f.loginMethod === "thirdparty" && f.id === factor)
);
}
const initalizedRecipes = getInitializedRecipes();
return {
emailPassword: tenantLoginMethods.emailPassword && initalizedRecipes.emailPassword,
passwordless: tenantLoginMethods.passwordless && initalizedRecipes.passwordless.enabled,
thirdParty: tenantLoginMethods.thirdParty && initalizedRecipes.thirdParty,
};
};

const loginMethods = getEnabledLoginMethods();

const hasNoLoginMethods = Object.values(loginMethods).every((value) => value === false);

if (hasNoLoginMethods) {
return (
<div className="block-small block-error tenant-no-login-methods-error">
<img
className="input-field-error-icon"
src={getImageUrl("form-field-error-icon.svg")}
alt="Error in field"
/>
<p className="input-field-error-text text-small text-error">
No login methods enabled for this tenant.
</p>
</div>
);
}

return (
<div className="tenant-login-methods">
{tenant.emailPassword.enabled && (
{loginMethods.emailPassword && (
<RecipePill
recipeId="emailpassword"
label="Email Password"
/>
)}
{tenant.passwordless.enabled && (
{loginMethods.passwordless && (
<RecipePill
recipeId="passwordless"
label="Passwordless"
/>
)}
{tenant.thirdParty.enabled && (
{loginMethods.thirdParty && (
<RecipePill
recipeId="thirdparty"
label="Third Party"
Expand Down
Loading

0 comments on commit b4ffa8c

Please sign in to comment.