mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 08:16:44 +00:00
2fa policy check working
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { formatAxiosError, internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import {
|
||||
CheckOrgUserAccessResponse,
|
||||
GetOrgResponse,
|
||||
ListUserOrgsResponse
|
||||
} from "@server/routers/org";
|
||||
import { GetOrgUserResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -11,6 +15,9 @@ import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvide
|
||||
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { build } from "@server/build";
|
||||
import OrgPolicyResult from "@app/components/OrgPolicyResult";
|
||||
import UserProvider from "@app/providers/UserProvider";
|
||||
import { Layout } from "@app/components/Layout";
|
||||
|
||||
export default async function OrgLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -32,25 +39,46 @@ export default async function OrgLayout(props: {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
let accessRes: CheckOrgUserAccessResponse | null = null;
|
||||
try {
|
||||
const getOrgUser = cache(() =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
`/org/${orgId}/user/${user.userId}`,
|
||||
const checkOrgAccess = cache(() =>
|
||||
internal.get<AxiosResponse<CheckOrgUserAccessResponse>>(
|
||||
`/org/${orgId}/user/${user.userId}/check`,
|
||||
cookie
|
||||
)
|
||||
);
|
||||
const orgUser = await getOrgUser();
|
||||
} catch {
|
||||
const res = await checkOrgAccess();
|
||||
accessRes = res.data.data;
|
||||
} catch (e) {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
try {
|
||||
const getOrg = cache(() =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(`/org/${orgId}`, cookie)
|
||||
if (!accessRes?.allowed) {
|
||||
// For non-admin users, show the member resources portal
|
||||
let orgs: ListUserOrgsResponse["orgs"] = [];
|
||||
try {
|
||||
const getOrgs = cache(async () =>
|
||||
internal.get<AxiosResponse<ListUserOrgsResponse>>(
|
||||
`/user/${user.userId}/orgs`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrgs();
|
||||
if (res && res.data.data.orgs) {
|
||||
orgs = res.data.data.orgs;
|
||||
}
|
||||
} catch (e) {}
|
||||
return (
|
||||
<UserProvider user={user}>
|
||||
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
|
||||
<OrgPolicyResult
|
||||
orgId={orgId}
|
||||
userId={user.userId}
|
||||
accessRes={accessRes}
|
||||
/>
|
||||
</Layout>
|
||||
</UserProvider>
|
||||
);
|
||||
await getOrg();
|
||||
} catch {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
let subscriptionStatus = null;
|
||||
|
||||
3
src/app/[orgId]/policy/page.tsx
Normal file
3
src/app/[orgId]/policy/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function OrgPolicyPage() {
|
||||
return <div>Org Policy Page</div>;
|
||||
}
|
||||
@@ -42,11 +42,15 @@ import {
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
// Schema for general organization settings
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string(),
|
||||
subnet: z.string().optional()
|
||||
subnet: z.string().optional(),
|
||||
requireTwoFactor: z.boolean().optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -60,6 +64,8 @@ export default function GeneralPage() {
|
||||
const { user } = useUserContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscriptionStatus = useSubscriptionStatusContext();
|
||||
|
||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||
const [loadingSave, setLoadingSave] = useState(false);
|
||||
@@ -69,7 +75,8 @@ export default function GeneralPage() {
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: org?.org.name,
|
||||
subnet: org?.org.subnet || "" // Add default value for subnet
|
||||
subnet: org?.org.subnet || "", // Add default value for subnet
|
||||
requireTwoFactor: org?.org.requireTwoFactor || false
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -129,11 +136,15 @@ export default function GeneralPage() {
|
||||
setLoadingSave(true);
|
||||
|
||||
try {
|
||||
// Update organization
|
||||
await api.post(`/org/${org?.org.orgId}`, {
|
||||
const reqData = {
|
||||
name: data.name
|
||||
// subnet: data.subnet // Include subnet in the API request
|
||||
});
|
||||
} as any;
|
||||
if (build !== "oss") {
|
||||
reqData.requireTwoFactor = data.requireTwoFactor || false;
|
||||
}
|
||||
|
||||
// Update organization
|
||||
await api.post(`/org/${org?.org.orgId}`, reqData);
|
||||
|
||||
// Also save auth page settings if they have unsaved changes
|
||||
if (
|
||||
@@ -168,9 +179,7 @@ export default function GeneralPage() {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("orgQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("orgQuestionRemove")}</p>
|
||||
<p>{t("orgMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
@@ -241,45 +250,122 @@ export default function GeneralPage() {
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{(build === "saas") && (
|
||||
<AuthPageSettings ref={authPageSettingsRef} />
|
||||
)}
|
||||
{/* Security Settings Section */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsSectionTitle>
|
||||
{t("securitySettings")}
|
||||
</SettingsSectionTitle>
|
||||
{build === "enterprise" && !isUnlocked() ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{build === "enterprise"
|
||||
? t("licenseBadge")
|
||||
: t("subscriptionBadge")}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<SettingsSectionDescription>
|
||||
{t("securitySettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="security-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireTwoFactor"
|
||||
render={({ field }) => {
|
||||
const isEnterpriseNotLicensed =
|
||||
build === "enterprise" &&
|
||||
!isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" &&
|
||||
!subscriptionStatus?.isSubscribed();
|
||||
const isDisabled =
|
||||
isEnterpriseNotLicensed ||
|
||||
isSaasNotSubscribed;
|
||||
const shouldDisableToggle = isDisabled;
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="require-two-factor"
|
||||
defaultChecked={
|
||||
field.value ||
|
||||
false
|
||||
}
|
||||
label={t(
|
||||
"requireTwoFactorForAllUsers"
|
||||
)}
|
||||
disabled={
|
||||
shouldDisableToggle
|
||||
}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) => {
|
||||
if (
|
||||
!shouldDisableToggle
|
||||
) {
|
||||
form.setValue(
|
||||
"requireTwoFactor",
|
||||
val
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{isDisabled
|
||||
? t(
|
||||
"requireTwoFactorDisabledDescription"
|
||||
)
|
||||
: t(
|
||||
"requireTwoFactorDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
{build !== "saas" && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center gap-2"
|
||||
loading={loadingDelete}
|
||||
disabled={loadingDelete}
|
||||
>
|
||||
{t("orgDelete")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="org-settings-form"
|
||||
loading={loadingSave}
|
||||
disabled={loadingSave}
|
||||
>
|
||||
{t("saveGeneralSettings")}
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{build !== "saas" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("orgDangerZone")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("orgDangerZoneDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center gap-2"
|
||||
loading={loadingDelete}
|
||||
disabled={loadingDelete}
|
||||
>
|
||||
{t("orgDelete")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ import { build } from "@server/build";
|
||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||
import { DomainRow } from "../../../../../../components/DomainsTable";
|
||||
import { toASCII, toUnicode } from "punycode";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
|
||||
export default function GeneralForm() {
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
@@ -65,6 +68,9 @@ export default function GeneralForm() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const [editDomainOpen, setEditDomainOpen] = useState(false);
|
||||
const {licenseStatus } = useLicenseStatusContext();
|
||||
const subscriptionStatus = useSubscriptionStatusContext();
|
||||
const {user} = useUserContext();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import { headers } from "next/headers";
|
||||
import { GetLoginPageResponse } from "@server/routers/loginPage/types";
|
||||
import { GetOrgTierResponse } from "@server/routers/billing/types";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { CheckOrgUserAccessResponse } from "@server/routers/org";
|
||||
import OrgPolicyRequired from "@app/components/OrgPolicyRequired";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -136,6 +138,34 @@ export default async function ResourceAuthPage(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = await authCookieHeader();
|
||||
|
||||
// Check org policy compliance before proceeding
|
||||
let orgPolicyCheck: CheckOrgUserAccessResponse | null = null;
|
||||
if (user && authInfo.orgId) {
|
||||
try {
|
||||
const policyRes = await internal.get<
|
||||
AxiosResponse<CheckOrgUserAccessResponse>
|
||||
>(`/org/${authInfo.orgId}/user/${user.userId}/check`, cookie);
|
||||
|
||||
orgPolicyCheck = policyRes.data.data;
|
||||
} catch (e) {
|
||||
console.error(formatAxiosError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// If user is not compliant with org policies, show policy requirements
|
||||
if (orgPolicyCheck && !orgPolicyCheck.allowed && orgPolicyCheck.policies) {
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<OrgPolicyRequired
|
||||
orgId={authInfo.orgId}
|
||||
policies={orgPolicyCheck.policies}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasAuth) {
|
||||
// no authentication so always go straight to the resource
|
||||
redirect(redirectUrl);
|
||||
@@ -151,7 +181,7 @@ export default async function ResourceAuthPage(props: {
|
||||
>(
|
||||
`/resource/${authInfo.resourceId}/get-exchange-token`,
|
||||
{},
|
||||
await authCookieHeader()
|
||||
cookie
|
||||
);
|
||||
|
||||
if (res.data.data.requestToken) {
|
||||
|
||||
59
src/components/OrgPolicyRequired.tsx
Normal file
59
src/components/OrgPolicyRequired.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@app/components/ui/card";
|
||||
import { Shield, ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type OrgPolicyRequiredProps = {
|
||||
orgId: string;
|
||||
policies: {
|
||||
requiredTwoFactor?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export default function OrgPolicyRequired({
|
||||
orgId,
|
||||
policies
|
||||
}: OrgPolicyRequiredProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const policySteps = [];
|
||||
|
||||
if (policies?.requiredTwoFactor === false) {
|
||||
policySteps.push(t("enableTwoFactorAuthentication"));
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-orange-100">
|
||||
<Shield className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
<CardTitle className="text-xl font-semibold">
|
||||
{t("additionalSecurityRequired")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("organizationRequiresAdditionalSteps")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="pt-4">
|
||||
<Link href={`/${orgId}`}>
|
||||
<Button className="w-full">
|
||||
{t("completeSecuritySteps")}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
195
src/components/OrgPolicyResult.tsx
Normal file
195
src/components/OrgPolicyResult.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CheckOrgUserAccessResponse } from "@server/routers/org";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { CheckCircle2, XCircle, Shield } from "lucide-react";
|
||||
import Enable2FaDialog from "./Enable2FaDialog";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type OrgPolicyResultProps = {
|
||||
orgId: string;
|
||||
userId: string;
|
||||
accessRes: CheckOrgUserAccessResponse;
|
||||
};
|
||||
|
||||
type PolicyItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
compliant: boolean;
|
||||
action?: () => void;
|
||||
actionText?: string;
|
||||
};
|
||||
|
||||
export default function OrgPolicyResult({
|
||||
orgId,
|
||||
userId,
|
||||
accessRes
|
||||
}: OrgPolicyResultProps) {
|
||||
const [show2FaDialog, setShow2FaDialog] = useState(false);
|
||||
const t = useTranslations();
|
||||
const { user } = useUserContext();
|
||||
const router = useRouter();
|
||||
|
||||
// Determine if user is compliant with 2FA policy
|
||||
const isTwoFactorCompliant = user?.twoFactorEnabled || false;
|
||||
const policyKeys = Object.keys(accessRes.policies || {});
|
||||
|
||||
const policies: PolicyItem[] = [];
|
||||
|
||||
// Only add 2FA policy if the organization has it enforced
|
||||
if (policyKeys.includes("requiredTwoFactor")) {
|
||||
policies.push({
|
||||
id: "two-factor",
|
||||
name: t("twoFactorAuthentication"),
|
||||
description: t("twoFactorDescription"),
|
||||
compliant: isTwoFactorCompliant,
|
||||
action: !isTwoFactorCompliant
|
||||
? () => setShow2FaDialog(true)
|
||||
: undefined,
|
||||
actionText: !isTwoFactorCompliant ? t("enableTwoFactor") : undefined
|
||||
});
|
||||
|
||||
// policies.push({
|
||||
// id: "reauth-required",
|
||||
// name: "Re-authentication",
|
||||
// description:
|
||||
// "It's been 30 days since you last verified your identity. Please log out and log back in to continue.",
|
||||
// compliant: false,
|
||||
// action: () => {},
|
||||
// actionText: "Log Out"
|
||||
// });
|
||||
//
|
||||
// policies.push({
|
||||
// id: "password-rotation",
|
||||
// name: "Password Rotation",
|
||||
// description:
|
||||
// "It's been 30 days since you last changed your password. Please update your password to continue.",
|
||||
// compliant: false,
|
||||
// action: () => {},
|
||||
// actionText: "Change Password"
|
||||
// });
|
||||
}
|
||||
|
||||
const nonCompliantPolicies = policies.filter((policy) => !policy.compliant);
|
||||
const allCompliant =
|
||||
policies.length === 0 || nonCompliantPolicies.length === 0;
|
||||
|
||||
// Calculate progress
|
||||
const completedPolicies = policies.filter(
|
||||
(policy) => policy.compliant
|
||||
).length;
|
||||
const totalPolicies = policies.length;
|
||||
const progressPercentage =
|
||||
totalPolicies > 0 ? (completedPolicies / totalPolicies) * 100 : 100;
|
||||
|
||||
// If no policies are enforced, show a simple success message
|
||||
if (policies.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle2 className="mx-auto h-12 w-12 text-green-500 mb-4" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{t("accessGranted")}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
{t("noSecurityRequirements")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{t("securityRequirements")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{allCompliant
|
||||
? t("allRequirementsMet")
|
||||
: t("completeRequirementsToContinue")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="px-6 pb-4">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>
|
||||
{completedPolicies} of {totalPolicies} steps
|
||||
completed
|
||||
</span>
|
||||
<span>{Math.round(progressPercentage)}%</span>
|
||||
</div>
|
||||
<Progress value={progressPercentage} className="h-2" />
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{policies.map((policy) => (
|
||||
<div
|
||||
key={policy.id}
|
||||
className="flex items-start gap-3 p-4 border rounded-lg"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{policy.compliant ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium">
|
||||
{policy.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{policy.description}
|
||||
</p>
|
||||
{policy.action && policy.actionText && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={policy.action}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{policy.actionText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{allCompliant && (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-green-600 font-medium">
|
||||
{t("allRequirementsMet")}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{t("youCanNowAccessOrganization")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Enable2FaDialog
|
||||
open={show2FaDialog}
|
||||
setOpen={(val) => {
|
||||
setShow2FaDialog(val);
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user