diff --git a/app/components/modals/Login/LoginForm.tsx b/app/components/modals/Login/LoginForm.tsx
index f354b08..3435ef8 100644
--- a/app/components/modals/Login/LoginForm.tsx
+++ b/app/components/modals/Login/LoginForm.tsx
@@ -9,6 +9,7 @@ import TextInput from "../../controls/TextInput";
import ContentLockup from "../../type/ContentLockup";
import Alert from "../Alert";
import { requestMagicLink } from "../../../../lib/create/api";
+import { EMAIL_MAX_LEN } from "../../../../lib/create/isValidCreateFlowSaveEmail";
import { buildCreateFlowDraftPayload } from "../../../../lib/create/buildCreateFlowDraftPayload";
import { safeInternalPath } from "../../../../lib/safeInternalPath";
import {
@@ -232,6 +233,7 @@ export default function LoginForm({
disabled={submitting}
error={Boolean(emailError)}
showHelpIcon
+ maxLength={EMAIL_MAX_LEN}
/>
{emailError ? (
(null);
export function AuthModalProvider({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const [opts, setOpts] = useState({});
- const t = useTranslation("pages.login");
const openLogin = useCallback((o?: OpenLoginOptions) => {
setOpts(o ?? {});
@@ -62,15 +59,6 @@ export function AuthModalProvider({ children }: { children: ReactNode }) {
backdropVariant={backdropVariant}
usePortal
ariaLabelledBy="login-modal-heading"
- belowCard={
- closeLogin()}
- >
- {t("backToHome")}
-
- }
>
;
export const magicLinkRequestBodySchema = z.object({
- email: z.string(),
+ email: z.string().max(EMAIL_MAX_LEN),
next: z.string().optional(),
draft: createFlowStateSchema.optional(),
});
diff --git a/stories/modals/Login.stories.tsx b/stories/modals/Login.stories.tsx
index c99ad88..0baade4 100644
--- a/stories/modals/Login.stories.tsx
+++ b/stories/modals/Login.stories.tsx
@@ -95,7 +95,7 @@ export const HeaderOverlayBlurred = {
docs: {
description: {
story:
- 'Same as **Log in** from the site header: `backdropVariant="blurredYellow"`, `usePortal`, card + “Back to home” below.',
+ 'Same as **Log in** from the site header: `backdropVariant="blurredYellow"`, `usePortal`, no “Back to home” under the card.',
},
},
},
@@ -107,14 +107,6 @@ export const HeaderOverlayBlurred = {
backdropVariant="blurredYellow"
usePortal
ariaLabelledBy="login-modal-heading"
- belowCard={
-
- {backToHome}
-
- }
>
Loading…
}>
diff --git a/tests/components/LoginForm.test.tsx b/tests/components/LoginForm.test.tsx
index aa8ccea..58e113e 100644
--- a/tests/components/LoginForm.test.tsx
+++ b/tests/components/LoginForm.test.tsx
@@ -43,6 +43,7 @@ vi.mock("../../app/(app)/create/utils/anonymousDraftStorage", async (importOrigi
};
});
+import { EMAIL_MAX_LEN } from "../../lib/create/isValidCreateFlowSaveEmail";
import { requestMagicLink } from "../../lib/create/api";
import { setTransferPendingFlag } from "../../app/(app)/create/utils/anonymousDraftStorage";
@@ -70,7 +71,7 @@ describe("LoginForm", () => {
).toBeInTheDocument();
expect(
screen.getByRole("textbox", { name: /email address/i }),
- ).toBeInTheDocument();
+ ).toHaveAttribute("maxLength", String(EMAIL_MAX_LEN));
expect(
screen.getByRole("button", { name: /send me a magic link/i }),
).toBeInTheDocument();
@@ -126,6 +127,24 @@ describe("LoginForm", () => {
expect(screen.getByText(/we sent a sign-in link/i)).toBeInTheDocument();
});
+ it("submits a long email without treating length as invalid", async () => {
+ const user = userEvent.setup();
+ const email =
+ "very.long.local-part.for.testing@subdomain.example.communityrule.org";
+ vi.mocked(requestMagicLink).mockResolvedValue({ ok: true });
+ renderLoginForm();
+ await user.type(
+ screen.getByRole("textbox", { name: /email address/i }),
+ email,
+ );
+ await user.click(
+ screen.getByRole("button", { name: /send me a magic link/i }),
+ );
+ await waitFor(() => {
+ expect(requestMagicLink).toHaveBeenCalledWith(email, "/", undefined);
+ });
+ });
+
it("saveProgress variant uses magicLinkNextPath and sets transfer pending on success", async () => {
const user = userEvent.setup();
vi.mocked(requestMagicLink).mockResolvedValue({ ok: true });
diff --git a/tests/contexts/AuthModalContext.test.tsx b/tests/contexts/AuthModalContext.test.tsx
index 86e432d..c17bb78 100644
--- a/tests/contexts/AuthModalContext.test.tsx
+++ b/tests/contexts/AuthModalContext.test.tsx
@@ -96,8 +96,8 @@ describe("AuthModalProvider (header overlay)", () => {
screen.getByRole("heading", { name: /log in to communityrule/i }),
).toBeInTheDocument();
expect(
- screen.getByRole("link", { name: /back to home/i }),
- ).toBeInTheDocument();
+ screen.queryByRole("link", { name: /back to home/i }),
+ ).not.toBeInTheDocument();
});
it("closes overlay when closeLogin is called", async () => {
diff --git a/tests/unit/authMagicLinkRequestRoute.test.ts b/tests/unit/authMagicLinkRequestRoute.test.ts
index 0b13044..897e9a1 100644
--- a/tests/unit/authMagicLinkRequestRoute.test.ts
+++ b/tests/unit/authMagicLinkRequestRoute.test.ts
@@ -113,6 +113,63 @@ describe("POST /api/auth/magic-link/request", () => {
);
});
+ it("accepts an email longer than 48 characters", async () => {
+ const email =
+ "very.long.local-part.for.testing@subdomain.example.communityrule.org";
+ expect(email.length).toBeGreaterThan(48);
+ const res = await POST(
+ new NextRequest("https://x.test/api/auth/magic-link/request", {
+ method: "POST",
+ body: JSON.stringify({ email }),
+ headers: { "content-type": "application/json" },
+ }),
+ undefined,
+ );
+ expect(res.status).toBe(200);
+ expect(sendMagicLinkEmailMock).toHaveBeenCalledWith(
+ email,
+ expect.stringContaining("/api/auth/magic-link/verify?token="),
+ );
+ });
+
+ it("rejects an email longer than 254 characters", async () => {
+ const email = `${"a".repeat(243)}@example.com`;
+ expect(email.length).toBeGreaterThan(254);
+ const res = await POST(
+ new NextRequest("https://x.test/api/auth/magic-link/request", {
+ method: "POST",
+ body: JSON.stringify({ email }),
+ headers: { "content-type": "application/json" },
+ }),
+ undefined,
+ );
+ expect(res.status).toBe(400);
+ expect(createMock).not.toHaveBeenCalled();
+ });
+
+ it("accepts a draft whose method-card support text is longer than 48 characters", async () => {
+ const res = await POST(
+ new NextRequest("https://x.test/api/auth/magic-link/request", {
+ method: "POST",
+ body: JSON.stringify({
+ email: "a@b.c",
+ draft: {
+ customMethodCardMetaById: {
+ "00000000-0000-4000-8000-000000000001": {
+ label: "Signal",
+ supportText:
+ "A decision is assumed approved unless objections are raised within a specified timeframe.",
+ },
+ },
+ },
+ }),
+ headers: { "content-type": "application/json" },
+ }),
+ undefined,
+ );
+ expect(res.status).toBe(200);
+ });
+
it("returns 502 and rolls back the token when mail fails", async () => {
sendMagicLinkEmailMock.mockRejectedValueOnce(new Error("smtp down"));
const res = await POST(
diff --git a/tests/unit/createFlowValidation.test.ts b/tests/unit/createFlowValidation.test.ts
index 31fbe22..c3af55e 100644
--- a/tests/unit/createFlowValidation.test.ts
+++ b/tests/unit/createFlowValidation.test.ts
@@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
+import { CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS } from "../../lib/create/customMethodCardWizardConstants";
import {
assertPlainJsonValue,
DEFAULT_PLAIN_JSON_LIMITS,
@@ -87,6 +88,33 @@ describe("createFlowStateSchema", () => {
expect(r.success).toBe(false);
});
+ it("accepts custom method card support text longer than 48 characters", () => {
+ const r = createFlowStateSchema.safeParse({
+ customMethodCardMetaById: {
+ "00000000-0000-4000-8000-000000000001": {
+ label: "Signal",
+ supportText:
+ "A decision is assumed approved unless objections are raised within a specified timeframe.",
+ },
+ },
+ });
+ expect(r.success).toBe(true);
+ });
+
+ it("rejects custom method card support text over the description max", () => {
+ const r = createFlowStateSchema.safeParse({
+ customMethodCardMetaById: {
+ "00000000-0000-4000-8000-000000000001": {
+ label: "Signal",
+ supportText: "x".repeat(
+ CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS + 1,
+ ),
+ },
+ },
+ });
+ expect(r.success).toBe(false);
+ });
+
it("accepts communityStructureChipSnapshots with custom chip rows", () => {
const r = createFlowStateSchema.safeParse({
communityStructureChipSnapshots: {