chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,152 @@
import { Form } from "@remix-run/react";
import { useState } from "react";
import { Button } from "~/components/primitives/Buttons";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/primitives/Dialog";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormError } from "~/components/primitives/FormError";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/primitives/InputOTP";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Spinner } from "~/components/primitives/Spinner";
interface MfaDisableDialogProps {
isOpen: boolean;
isSubmitting: boolean;
error?: string;
onDisable: (totpCode?: string, recoveryCode?: string) => void;
onCancel: () => void;
}
export function MfaDisableDialog({
isOpen,
isSubmitting,
error,
onDisable,
onCancel,
}: MfaDisableDialogProps) {
const [totpCode, setTotpCode] = useState("");
const [recoveryCode, setRecoveryCode] = useState("");
const [useRecoveryCode, setUseRecoveryCode] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onDisable(useRecoveryCode ? undefined : totpCode, useRecoveryCode ? recoveryCode : undefined);
};
const handleCancel = () => {
setTotpCode("");
setRecoveryCode("");
setUseRecoveryCode(false);
onCancel();
};
const handleSwitchToRecoveryCode = () => {
setUseRecoveryCode(true);
setTotpCode("");
};
const handleSwitchToTotpCode = () => {
setUseRecoveryCode(false);
setRecoveryCode("");
};
return (
<Dialog open={isOpen} onOpenChange={handleCancel}>
<DialogContent>
<DialogHeader>
<DialogTitle>Disable multi-factor authentication</DialogTitle>
</DialogHeader>
<Form method="post" onSubmit={handleSubmit}>
{useRecoveryCode ? (
<div className="pt-3">
<Paragraph className="mb-6 text-center">
Enter one of your recovery codes to disable MFA.
</Paragraph>
<Fieldset className="flex w-full flex-col items-center gap-y-2">
<InputGroup>
<Input
type="password"
name="recoveryCode"
spellCheck={false}
placeholder="Enter recovery code"
variant="large"
required
autoFocus
value={recoveryCode}
onChange={(e) => setRecoveryCode(e.target.value)}
/>
</InputGroup>
</Fieldset>
<div className="flex justify-center">
<Button
type="button"
onClick={handleSwitchToTotpCode}
variant="minimal/small"
className="my-4"
>
Use an authenticator app
</Button>
</div>
</div>
) : (
<div className="pt-3">
<Paragraph variant="base" className="mb-6 text-center">
Enter the code from your authenticator app to disable MFA.
</Paragraph>
<Fieldset className="flex w-full flex-col items-center gap-y-2">
<InputOTP
maxLength={6}
value={totpCode}
onChange={(value) => setTotpCode(value)}
variant="large"
>
<InputOTPGroup variant="large" fullWidth>
<InputOTPSlot index={0} autoFocus variant="large" fullWidth />
<InputOTPSlot index={1} variant="large" fullWidth />
<InputOTPSlot index={2} variant="large" fullWidth />
<InputOTPSlot index={3} variant="large" fullWidth />
<InputOTPSlot index={4} variant="large" fullWidth />
<InputOTPSlot index={5} variant="large" fullWidth />
</InputOTPGroup>
</InputOTP>
</Fieldset>
<div className="flex justify-center">
<Button
type="button"
onClick={handleSwitchToRecoveryCode}
variant="minimal/small"
className="my-4"
>
Use a recovery code
</Button>
</div>
</div>
)}
{error && <FormError>{error}</FormError>}
<DialogFooter>
<Button type="button" variant="secondary/medium" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" variant="primary/medium" disabled={isSubmitting}>
{isSubmitting ? <Spinner className="mr-2 size-5" color="white" /> : null}
{isSubmitting ? (
<span className="text-text-bright">Disabling</span>
) : (
<span className="text-text-bright">Disable MFA</span>
)}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,204 @@
import { Form } from "@remix-run/react";
import { DownloadIcon } from "lucide-react";
import { QRCodeSVG } from "qrcode.react";
import { useState } from "react";
import { Button } from "~/components/primitives/Buttons";
import { CopyableText } from "~/components/primitives/CopyableText";
import { CopyButton } from "~/components/primitives/CopyButton";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/primitives/Dialog";
import { FormError } from "~/components/primitives/FormError";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/primitives/InputOTP";
import { Paragraph } from "~/components/primitives/Paragraph";
interface MfaSetupDialogProps {
isOpen: boolean;
setupData?: {
secret: string;
otpAuthUrl: string;
};
recoveryCodes?: string[];
error?: string;
isSubmitting: boolean;
onValidate: (code: string) => void;
onCancel: () => void;
onSaveRecoveryCodes: () => void;
}
export function MfaSetupDialog({
isOpen,
setupData,
recoveryCodes,
error,
isSubmitting,
onValidate,
onCancel,
onSaveRecoveryCodes,
}: MfaSetupDialogProps) {
const [totpCode, setTotpCode] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onValidate(totpCode);
setTotpCode("");
};
const handleCancel = () => {
setTotpCode("");
onCancel();
};
const handleRecoverySubmit = (e: React.FormEvent) => {
e.preventDefault();
onSaveRecoveryCodes();
};
const downloadRecoveryCodes = () => {
if (!recoveryCodes) return;
const content = recoveryCodes.join("\n");
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "trigger-dev-recovery-codes.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Show recovery codes if they exist
if (recoveryCodes && recoveryCodes.length > 0) {
return (
<Dialog open={isOpen}>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Recovery codes</DialogTitle>
</DialogHeader>
<Form method="post" onSubmit={handleRecoverySubmit}>
<div className="flex flex-col gap-2 pb-0 pt-3">
<Paragraph spacing>
Copy and store these recovery codes carefully in case you lose your device.
</Paragraph>
<div className="flex flex-col rounded border border-grid-dimmed bg-background-bright">
<div className="grid grid-cols-3 gap-x-2 gap-y-4 px-3 py-6">
{recoveryCodes.map((code, index) => (
<span key={index} className="text-center font-mono text-xs text-text-bright">
{code}
</span>
))}
</div>
<div className="flex items-center justify-end border-t border-grid-bright px-1.5 py-1.5">
<Button
type="button"
variant="minimal/medium"
onClick={downloadRecoveryCodes}
LeadingIcon={DownloadIcon}
>
Download
</Button>
<CopyButton
value={recoveryCodes.join("\n")}
buttonVariant="minimal"
showTooltip={false}
>
Copy
</CopyButton>
</div>
</div>
</div>
<DialogFooter className="justify-end border-t-0">
<Button
type="submit"
variant="primary/medium"
shortcut={{ key: "Enter" }}
hideShortcutKey
autoFocus
>
Continue
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}
// Show QR setup if no recovery codes yet
if (!setupData) return null;
return (
<Dialog open={isOpen}>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Enable authenticator app</DialogTitle>
</DialogHeader>
<Form method="post" onSubmit={handleSubmit}>
<div className="flex flex-col gap-4 pt-3">
<Paragraph>
Scan the QR code below with your preferred authenticator app then enter the 6 digit
code that the app generates. Alternatively, you can copy the secret below and paste it
into your app.
</Paragraph>
<div className="flex flex-col items-center justify-center gap-y-4 rounded border border-grid-dimmed bg-background-bright py-4">
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<QRCodeSVG value={setupData.otpAuthUrl} size={300} marginSize={3} />
</div>
<CopyableText value={setupData.secret} className="font-mono text-sm tracking-wide" />
</div>
<div className="mb-4 flex items-center justify-center">
<InputOTP
maxLength={6}
value={totpCode}
onChange={(value) => setTotpCode(value)}
variant="large"
name="totpCode"
onKeyDown={(e) => {
if (e.key === "Enter" && totpCode.length === 6) {
handleSubmit(e);
}
}}
>
<InputOTPGroup variant="large">
<InputOTPSlot index={0} variant="large" autoFocus />
<InputOTPSlot index={1} variant="large" />
<InputOTPSlot index={2} variant="large" />
<InputOTPSlot index={3} variant="large" />
<InputOTPSlot index={4} variant="large" />
<InputOTPSlot index={5} variant="large" />
</InputOTPGroup>
</InputOTP>
</div>
</div>
<div className="mb-4 flex justify-center">{error && <FormError>{error}</FormError>}</div>
<DialogFooter>
<Button type="button" variant="secondary/medium" onClick={handleCancel}>
Cancel
</Button>
<Button
type="submit"
variant="primary/medium"
disabled={totpCode.length !== 6 || isSubmitting}
shortcut={{ key: "Enter" }}
hideShortcutKey
>
Confirm
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,35 @@
import { Form } from "@remix-run/react";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Switch } from "~/components/primitives/Switch";
interface MfaToggleProps {
isEnabled: boolean;
onToggle: (enabled: boolean) => void;
}
export function MfaToggle({ isEnabled, onToggle }: MfaToggleProps) {
return (
<Form method="post" className="w-full">
<div className="flex w-full items-center justify-between gap-4">
<InputGroup className="flex-1">
<Label htmlFor="mfa">Multi-factor authentication</Label>
<Paragraph variant="small">
Require a one-time code from your authenticator app (TOTP).
</Paragraph>
</InputGroup>
<div className="flex flex-none items-center">
<Switch
id="mfa"
variant="medium"
labelPosition="right"
className="w-fit pr-3"
checked={isEnabled}
onCheckedChange={onToggle}
/>
</div>
</div>
</Form>
);
}
@@ -0,0 +1,186 @@
import { type ActionFunctionArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import { z } from "zod";
import {
redirectWithSuccessMessage,
redirectWithErrorMessage,
typedJsonWithSuccessMessage,
} from "~/models/message.server";
import { MultiFactorAuthenticationService } from "~/services/mfa/multiFactorAuthentication.server";
import { requireUserId } from "~/services/session.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { useMfaSetup } from "./useMfaSetup";
import { MfaToggle } from "./MfaToggle";
import { MfaSetupDialog } from "./MfaSetupDialog";
import { MfaDisableDialog } from "./MfaDisableDialog";
const formSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("enable-mfa"),
}),
z.object({
action: z.literal("disable-mfa"),
totpCode: z.string().optional(),
recoveryCode: z.string().optional(),
}),
z.object({
action: z.literal("saved-recovery-codes"),
}),
z.object({
action: z.literal("cancel-totp"),
}),
z.object({
action: z.literal("validate-totp"),
totpCode: z.string().length(6, "TOTP code must be 6 digits"),
}),
]);
function validateForm(formData: FormData) {
const formEntries = Object.fromEntries(formData.entries());
const result = formSchema.safeParse(formEntries);
if (!result.success) {
return {
valid: false as const,
errors: result.error.flatten().fieldErrors,
};
}
return {
valid: true as const,
data: result.data,
};
}
export async function action({ request }: ActionFunctionArgs) {
try {
const userId = await requireUserId(request);
const formData = await request.formData();
const submission = validateForm(formData);
if (!submission.valid) {
return typedjson({
action: "invalid-form" as const,
errors: submission.errors,
});
}
const mfaSetupService = new MultiFactorAuthenticationService();
switch (submission.data.action) {
case "enable-mfa": {
const result = await mfaSetupService.enableTotp(userId);
return typedjson({
action: "enable-mfa" as const,
secret: result.secret,
otpAuthUrl: result.otpAuthUrl,
});
}
case "disable-mfa": {
const result = await mfaSetupService.disableTotp(userId, {
totpCode: submission.data.totpCode,
recoveryCode: submission.data.recoveryCode,
});
if (result.success) {
return typedJsonWithSuccessMessage(
{
action: "disable-mfa" as const,
success: true as const,
},
request,
"Successfully disabled MFA"
);
} else {
return typedjson({
action: "disable-mfa" as const,
success: false as const,
error: "Invalid code provided. Please try again.",
});
}
}
case "validate-totp": {
const result = await mfaSetupService.validateTotpSetup(userId, submission.data.totpCode);
if (result.success) {
return typedjson({
action: "validate-totp" as const,
success: true as const,
recoveryCodes: result.recoveryCodes,
});
} else {
return typedjson({
action: "validate-totp" as const,
success: false as const,
error: "Invalid code provided. Please try again.",
otpAuthUrl: result.otpAuthUrl,
secret: result.secret,
});
}
}
case "cancel-totp": {
return typedjson({
action: "cancel-totp" as const,
success: true as const,
});
}
case "saved-recovery-codes": {
return redirectWithSuccessMessage("/account/security", request, "Successfully enabled MFA");
}
}
} catch (error) {
if (error instanceof ServiceValidationError) {
return redirectWithErrorMessage("/account/security", request, error.message);
}
// Re-throw unexpected errors
throw error;
}
}
export function MfaSetup({ isEnabled }: { isEnabled: boolean }) {
const {
state,
actions,
isQrDialogOpen,
isRecoveryDialogOpen: _isRecoveryDialogOpen,
isDisableDialogOpen,
} = useMfaSetup(isEnabled);
const handleToggle = (enabled: boolean) => {
if (enabled && !state.isEnabled) {
actions.enableMfa();
} else if (!enabled && state.isEnabled) {
actions.openDisableDialog();
}
};
return (
<>
<MfaToggle isEnabled={state.isEnabled} onToggle={handleToggle} />
<MfaSetupDialog
isOpen={isQrDialogOpen}
setupData={state.setupData}
recoveryCodes={state.recoveryCodes}
error={state.error}
isSubmitting={state.isSubmitting}
onValidate={actions.validateTotp}
onCancel={actions.cancelSetup}
onSaveRecoveryCodes={actions.saveRecoveryCodes}
/>
<MfaDisableDialog
isOpen={isDisableDialogOpen}
isSubmitting={state.isSubmitting}
error={state.error}
onDisable={actions.disableMfa}
onCancel={actions.cancelDisable}
/>
</>
);
}
@@ -0,0 +1,301 @@
import { useReducer, useEffect } from "react";
import { useTypedFetcher } from "remix-typedjson";
import type { action } from "./route";
export type MfaPhase = "idle" | "enabling" | "validating" | "showing-recovery" | "disabling";
export interface MfaState {
phase: MfaPhase;
isEnabled: boolean;
setupData?: {
secret: string;
otpAuthUrl: string;
};
recoveryCodes?: string[];
error?: string;
isSubmitting: boolean;
disableMethod: "totp" | "recovery";
}
export type MfaAction =
| { type: "ENABLE_MFA" }
| { type: "SETUP_DATA_RECEIVED"; setupData: { secret: string; otpAuthUrl: string } }
| { type: "CANCEL_SETUP" }
| { type: "VALIDATE_TOTP"; code: string }
| { type: "VALIDATION_SUCCESS"; recoveryCodes: string[] }
| { type: "VALIDATION_FAILED"; error: string; setupData: { secret: string; otpAuthUrl: string } }
| { type: "RECOVERY_CODES_SAVED" }
| { type: "OPEN_DISABLE_DIALOG" }
| { type: "DISABLE_MFA" }
| { type: "DISABLE_SUCCESS" }
| { type: "DISABLE_FAILED"; error: string }
| { type: "CANCEL_DISABLE" }
| { type: "SET_DISABLE_METHOD"; method: "totp" | "recovery" }
| { type: "SET_ERROR"; error: string }
| { type: "CLEAR_ERROR" }
| { type: "SET_SUBMITTING"; isSubmitting: boolean };
function mfaReducer(state: MfaState, action: MfaAction): MfaState {
switch (action.type) {
case "ENABLE_MFA":
return {
...state,
phase: "enabling",
isSubmitting: true,
error: undefined,
};
case "SETUP_DATA_RECEIVED":
return {
...state,
phase: "enabling",
setupData: action.setupData,
error: undefined,
isSubmitting: false,
};
case "CANCEL_SETUP":
return {
...state,
phase: "idle",
setupData: undefined,
error: undefined,
isSubmitting: false,
};
case "VALIDATE_TOTP":
return {
...state,
phase: "validating",
isSubmitting: true,
error: undefined,
};
case "VALIDATION_SUCCESS":
return {
...state,
phase: "showing-recovery",
recoveryCodes: action.recoveryCodes,
isSubmitting: false,
isEnabled: true,
error: undefined,
};
case "VALIDATION_FAILED":
return {
...state,
phase: "enabling",
setupData: action.setupData,
error: action.error,
isSubmitting: false,
};
case "RECOVERY_CODES_SAVED":
return {
...state,
phase: "idle",
setupData: undefined,
recoveryCodes: undefined,
isSubmitting: false,
};
case "OPEN_DISABLE_DIALOG":
return {
...state,
phase: "disabling",
error: undefined,
isSubmitting: false,
};
case "DISABLE_MFA":
return {
...state,
isSubmitting: true,
error: undefined,
};
case "DISABLE_SUCCESS":
return {
...state,
phase: "idle",
isEnabled: false,
error: undefined,
isSubmitting: false,
};
case "DISABLE_FAILED":
return {
...state,
error: action.error,
isSubmitting: false,
};
case "CANCEL_DISABLE":
return {
...state,
phase: "idle",
error: undefined,
isSubmitting: false,
};
case "SET_DISABLE_METHOD":
return {
...state,
disableMethod: action.method,
error: undefined,
};
case "SET_ERROR":
return {
...state,
error: action.error,
};
case "CLEAR_ERROR":
return {
...state,
error: undefined,
};
case "SET_SUBMITTING":
return {
...state,
isSubmitting: action.isSubmitting,
};
default:
return state;
}
}
export function useMfaSetup(initialIsEnabled: boolean) {
const fetcher = useTypedFetcher<typeof action>();
const [state, dispatch] = useReducer(mfaReducer, {
phase: "idle",
isEnabled: initialIsEnabled,
isSubmitting: false,
disableMethod: "totp",
});
// Handle fetcher responses
useEffect(() => {
if (fetcher.data) {
const { data } = fetcher;
switch (data.action) {
case "enable-mfa":
dispatch({
type: "SETUP_DATA_RECEIVED",
setupData: { secret: data.secret, otpAuthUrl: data.otpAuthUrl },
});
break;
case "validate-totp":
if (data.success) {
dispatch({
type: "VALIDATION_SUCCESS",
recoveryCodes: data.recoveryCodes || [],
});
} else {
dispatch({
type: "VALIDATION_FAILED",
error: data.error || "Invalid code",
setupData: { secret: data.secret!, otpAuthUrl: data.otpAuthUrl! },
});
}
break;
case "disable-mfa":
if (data.success) {
dispatch({ type: "DISABLE_SUCCESS" });
} else {
dispatch({
type: "DISABLE_FAILED",
error: data.error || "Failed to disable MFA",
});
}
break;
case "cancel-totp":
dispatch({ type: "CANCEL_SETUP" });
break;
}
}
}, [fetcher.data]);
// Handle submitting state
useEffect(() => {
dispatch({ type: "SET_SUBMITTING", isSubmitting: fetcher.state === "submitting" });
}, [fetcher.state]);
const actions = {
enableMfa: () => {
dispatch({ type: "ENABLE_MFA" });
fetcher.submit(
{ action: "enable-mfa" },
{ method: "POST", action: "/resources/account/mfa/setup" }
);
},
cancelSetup: () => {
dispatch({ type: "CANCEL_SETUP" });
fetcher.submit(
{ action: "cancel-totp" },
{ method: "POST", action: "/resources/account/mfa/setup" }
);
},
validateTotp: (code: string) => {
dispatch({ type: "VALIDATE_TOTP", code });
fetcher.submit(
{ action: "validate-totp", totpCode: code },
{ method: "POST", action: "/resources/account/mfa/setup" }
);
},
saveRecoveryCodes: () => {
dispatch({ type: "RECOVERY_CODES_SAVED" });
fetcher.submit(
{ action: "saved-recovery-codes" },
{ method: "POST", action: "/resources/account/mfa/setup" }
);
},
openDisableDialog: () => {
dispatch({ type: "OPEN_DISABLE_DIALOG" });
},
disableMfa: (totpCode?: string, recoveryCode?: string) => {
dispatch({ type: "DISABLE_MFA" });
const formData: Record<string, string> = { action: "disable-mfa" };
if (totpCode) formData.totpCode = totpCode;
if (recoveryCode) formData.recoveryCode = recoveryCode;
fetcher.submit(formData, { method: "POST", action: "/resources/account/mfa/setup" });
},
cancelDisable: () => {
dispatch({ type: "CANCEL_DISABLE" });
},
setDisableMethod: (method: "totp" | "recovery") => {
dispatch({ type: "SET_DISABLE_METHOD", method });
},
clearError: () => {
dispatch({ type: "CLEAR_ERROR" });
},
};
return {
state,
actions,
// Computed properties for easier access
isQrDialogOpen:
(state.phase === "enabling" && !!state.setupData) ||
(state.phase === "showing-recovery" && !!state.recoveryCodes),
isRecoveryDialogOpen: false, // Recovery is now handled within the setup dialog
isDisableDialogOpen: state.phase === "disabling",
};
}