diff --git a/cli/commands/disableUser2fa.ts b/cli/commands/disableUser2fa.ts new file mode 100644 index 000000000..8b602c334 --- /dev/null +++ b/cli/commands/disableUser2fa.ts @@ -0,0 +1,60 @@ +import { CommandModule } from "yargs"; +import { db, users } from "@server/db"; +import { eq } from "drizzle-orm"; + +/** + * Disable 2FA for a user by email address. + */ +type DisableUser2faArgs = { + email: string; +}; + +export const disableUser2fa: CommandModule<{}, DisableUser2faArgs> = { + command: "disable-user-2fa", + describe: "Disable 2FA for a user (sets twoFactorEnabled=false, clears secret)", + builder: (yargs) => { + return yargs.option("email", { + type: "string", + demandOption: true, + describe: "User email address" + }); + }, + handler: async (argv: { email: string }) => { + try { + const { email } = argv; + console.log(`Looking for user with email: ${email}`); + + // Find the user by email + const [user] = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (!user) { + console.error(`User with email '${email}' not found`); + process.exit(1); + } + + if (!user.twoFactorEnabled) { + console.log(`2FA is already disabled for user '${email}'.`); + process.exit(0); + } + + // Update user: disable 2FA and clear secret + await db.update(users) + .set({ + twoFactorEnabled: false, + twoFactorSecret: null, + twoFactorSetupRequested: false + }) + .where(eq(users.userId, user.userId)); + + console.log(`2FA disabled for user '${email}'.`); + process.exit(0); + } catch (error) { + console.error("Error disabling 2FA:", error); + process.exit(1); + } + } +}; diff --git a/cli/index.ts b/cli/index.ts index 3664bb8f8..19585bc6f 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -10,6 +10,7 @@ import { clearLicenseKeys } from "./commands/clearLicenseKeys"; import { deleteClient } from "./commands/deleteClient"; import { generateOrgCaKeys } from "./commands/generateOrgCaKeys"; import { clearCertificates } from "./commands/clearCertificates"; +import { disableUser2fa } from "./commands/disableUser2fa"; yargs(hideBin(process.argv)) .scriptName("pangctl") @@ -21,5 +22,6 @@ yargs(hideBin(process.argv)) .command(deleteClient) .command(generateOrgCaKeys) .command(clearCertificates) + .command(disableUser2fa) .demandCommand() .help().argv; diff --git a/messages/en-US.json b/messages/en-US.json index 7cbf6b166..52981764b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2668,6 +2668,8 @@ "validPassword": "Valid Password", "validEmail": "Valid email", "validSSO": "Valid SSO", + "view": "View", + "configManaged": "Config Managed", "connectedClient": "Connected Client", "resourceBlocked": "Resource Blocked", "droppedByRule": "Dropped by Rule", diff --git a/src/components/DomainPageClient.tsx b/src/components/DomainPageClient.tsx index 31527c5b8..6d64e42ce 100644 --- a/src/components/DomainPageClient.tsx +++ b/src/components/DomainPageClient.tsx @@ -13,6 +13,8 @@ import DomainCertForm from "@app/components/DomainCertForm"; import { build } from "@server/build"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; +import { Lock } from "lucide-react"; +import { Badge } from "@app/components/ui/badge"; interface DomainPageClientProps { initialDomain: GetDomainResponse; @@ -49,7 +51,22 @@ export default function DomainPageClient({ <>
+ {domain.baseDomain} + {domain.configManaged && ( + + + {t("configManaged", { + fallback: "Config Managed" + })} + + )} + + } description={t("domainSettingDescription")} /> {env.flags.usePangolinDns && domain.failed ? ( @@ -90,4 +107,4 @@ export default function DomainPageClient({
); -} \ No newline at end of file +} diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index 2c3abeb1a..fda219f34 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -16,6 +16,7 @@ import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Badge } from "@app/components/ui/badge"; +import { Lock } from "lucide-react"; import { useTranslations } from "next-intl"; import CreateDomainForm from "@app/components/CreateDomainForm"; import { useToast } from "@app/hooks/useToast"; @@ -72,7 +73,11 @@ export default function DomainsTable({ domains, orgId }: Props) { const { org } = useOrgContext(); const queryClient = useQueryClient(); - const { data: rawDomains, isRefetching, refetch } = useQuery({ + const { + data: rawDomains, + isRefetching, + refetch + } = useQuery({ ...orgQueries.domains({ orgId }), initialData: domains as any, refetchInterval: durationToMs(10, "seconds") @@ -80,12 +85,15 @@ export default function DomainsTable({ domains, orgId }: Props) { const tableData = useMemo( () => - (rawDomains ?? []).map((d) => ({ - ...d, - baseDomain: toUnicode(d.baseDomain), - type: d.type ?? "", - errorMessage: d.errorMessage ?? null - } as DomainRow)), + (rawDomains ?? []).map( + (d) => + ({ + ...d, + baseDomain: toUnicode(d.baseDomain), + type: d.type ?? "", + errorMessage: d.errorMessage ?? null + }) as DomainRow + ), [rawDomains] ); @@ -198,12 +206,17 @@ export default function DomainsTable({ domains, orgId }: Props) { - + {t("failed", { fallback: "Failed" })} -

{errorMessage}

+

+ {errorMessage} +

@@ -220,12 +233,17 @@ export default function DomainsTable({ domains, orgId }: Props) { - + {t("pending")} -

{errorMessage}

+

+ {errorMessage} +

@@ -253,6 +271,25 @@ export default function DomainsTable({ domains, orgId }: Props) { ); + }, + cell: ({ row }) => { + const domain = row.original; + return ( + + {domain.baseDomain} + {domain.configManaged && ( + + + {t("configManaged", { + fallback: "Config Managed" + })} + + )} + + ); } }, ...(env.env.flags.usePangolinDns ? [typeColumn] : []), @@ -283,16 +320,18 @@ export default function DomainsTable({ domains, orgId }: Props) { {t("viewSettings")} - { - setSelectedDomain(domain); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - + {!domain.configManaged && ( + { + setSelectedDomain(domain); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + )} {domain.failed && ( @@ -315,7 +354,9 @@ export default function DomainsTable({ domains, orgId }: Props) { href={`/${orgId}/settings/domains/${domain.domainId}`} >