Files
pangolin/src/components/OrgPolicyResult.tsx
2025-10-27 09:52:25 -07:00

230 lines
8.2 KiB
TypeScript

"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 ChangePasswordDialog from "./ChangePasswordDialog";
import { useTranslations } from "next-intl";
import { useUserContext } from "@app/hooks/useUserContext";
import { useRouter } from "next/navigation";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { createApiClient } from "@app/lib/api";
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 [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false);
const t = useTranslations();
const { user } = useUserContext();
const router = useRouter();
let requireedSteps = 0;
let completedSteps = 0;
const { env } = useEnvContext();
const api = createApiClient({ env });
const policies: PolicyItem[] = [];
if (
accessRes.policies?.requiredTwoFactor === false ||
accessRes.policies?.requiredTwoFactor === true
) {
const isTwoFactorCompliant =
accessRes.policies?.requiredTwoFactor === true;
policies.push({
id: "two-factor",
name: t("twoFactorAuthentication"),
description: t("twoFactorDescription"),
compliant: isTwoFactorCompliant,
action: !isTwoFactorCompliant
? () => setShow2FaDialog(true)
: undefined,
actionText: !isTwoFactorCompliant ? t("enableTwoFactor") : undefined
});
requireedSteps += 1;
if (isTwoFactorCompliant) {
completedSteps += 1;
}
}
// Add max session length policy if the organization has it enforced
if (accessRes.policies?.maxSessionLength) {
const maxSessionPolicy = accessRes.policies?.maxSessionLength;
const maxHours = maxSessionPolicy.maxSessionLengthHours;
// Use hours if less than 24, otherwise convert to days
const useHours = maxHours < 24;
const maxTime = useHours ? maxHours : Math.round(maxHours / 24);
const descriptionKey = useHours
? "reauthenticationDescriptionHours"
: "reauthenticationDescription";
const description = useHours
? t(descriptionKey, { maxHours })
: t(descriptionKey, { maxDays: maxTime });
policies.push({
id: "max-session-length",
name: t("reauthenticationRequired"),
description,
compliant: maxSessionPolicy.compliant,
action: !maxSessionPolicy.compliant
? async () => {
try {
await api.post("/auth/logout", undefined);
router.push(`/auth/login?orgId=${orgId}`);
} catch (error) {
console.error("Error during logout:", error);
router.push(`/auth/login?orgId=${orgId}`);
}
}
: undefined,
actionText: !maxSessionPolicy.compliant
? t("reauthenticateNow")
: undefined
});
requireedSteps += 1;
if (maxSessionPolicy.compliant) {
completedSteps += 1;
}
}
// Add password age policy if the organization has it enforced
if (accessRes.policies?.passwordAge) {
const passwordAgePolicy = accessRes.policies.passwordAge;
const maxDays = passwordAgePolicy.maxPasswordAgeDays;
const daysAgo = Math.round(passwordAgePolicy.passwordAgeDays);
policies.push({
id: "password-age",
name: t("passwordExpiryRequired"),
description: t("passwordExpiryDescription", {
maxDays,
daysAgo
}),
compliant: passwordAgePolicy.compliant,
action: !passwordAgePolicy.compliant
? () => setShowChangePasswordDialog(true)
: undefined,
actionText: !passwordAgePolicy.compliant
? t("changePasswordNow")
: undefined
});
requireedSteps += 1;
if (passwordAgePolicy.compliant) {
completedSteps += 1;
}
}
const progressPercentage =
requireedSteps === 0 ? 100 : (completedSteps / requireedSteps) * 100;
const allCompliant = completedSteps === requireedSteps;
return (
<>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{t("securityRequirements")}
</CardTitle>
<CardDescription>
{allCompliant
? t("allRequirementsMet")
: t("completeRequirementsToContinue")}
</CardDescription>
</CardHeader>
<div className="px-6 pb-4">
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
<span>
{completedSteps} of {requireedSteps} 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>
<Enable2FaDialog
open={show2FaDialog}
setOpen={(val) => {
setShow2FaDialog(val);
router.refresh();
}}
/>
<ChangePasswordDialog
open={showChangePasswordDialog}
setOpen={(val) => {
setShowChangePasswordDialog(val);
router.refresh();
}}
/>
</>
);
}