diff --git a/app/actions/seedCredentials.ts b/app/actions/seedCredentials.ts index e38b2dd3..8cbc8f56 100644 --- a/app/actions/seedCredentials.ts +++ b/app/actions/seedCredentials.ts @@ -99,6 +99,39 @@ export async function saveSeedCredentialAction(input: { }; } +/** + * Hand a verification code to a sign-in that is paused waiting for one. + * + * The browser session is still open on the other side of this, holding the + * challenge page; the runner polls this row and types whatever lands here + * into the live form. Nothing is stored — the waiter clears the column the + * moment it reads it. + */ +export async function submitSeedVerificationCodeAction(input: { + projectId: string; + host: string; + code: string; +}): Promise<{ ok: true } | Err> { + const access = await requireOrg(input.projectId); + if (!access.ok) return access; + + const code = input.code.trim(); + if (!code) return { ok: false, error: "Enter the code the site sent you." }; + if (!/^[A-Za-z0-9-]{4,12}$/.test(code)) { + return { ok: false, error: "That doesn\u2019t look like a verification code." }; + } + + const { error } = await serviceClient() + .from("outreach_seed_credentials") + .update({ verification_code: code }) + .eq("organization_id", access.organizationId) + .eq("host", normalizeHost(input.host)); + if (error) return { ok: false, error: error.message }; + + revalidatePath(`/projects/${input.projectId}/leads`); + return { ok: true }; +} + export async function deleteSeedCredentialAction(input: { projectId: string; host: string; diff --git a/components/leads/seed-logins.tsx b/components/leads/seed-logins.tsx index 21d207ce..34a07602 100644 --- a/components/leads/seed-logins.tsx +++ b/components/leads/seed-logins.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { deleteSeedCredentialAction, saveSeedCredentialAction, + submitSeedVerificationCodeAction, } from "@/app/actions/seedCredentials"; import type { StoredSeedCredential } from "@/lib/outreach/seedCredentials"; @@ -34,6 +35,7 @@ export function SeedLogins({ const [password, setPassword] = useState(""); const [note, setNote] = useState(null); const [error, setError] = useState(null); + const [codes, setCodes] = useState>({}); const save = (targetHost?: string) => start(async () => { @@ -57,6 +59,24 @@ export function SeedLogins({ router.refresh(); }); + const sendCode = (h: string) => + start(async () => { + setError(null); + setNote(null); + const res = await submitSeedVerificationCodeAction({ + projectId, + host: h, + code: codes[h] ?? "", + }); + if (!res.ok) { + setError(res.error); + return; + } + setCodes((prev) => ({ ...prev, [h]: "" })); + setNote("Code sent to the waiting sign-in."); + router.refresh(); + }); + const remove = (h: string) => start(async () => { setError(null); @@ -187,6 +207,37 @@ export function SeedLogins({

{c.lastError}

)} + {c.verificationPrompt && ( +
+

+ Waiting for a verification code. {c.verificationPrompt} +

+
+ + setCodes((prev) => ({ ...prev, [c.host]: e.target.value })) + } + /> + +
+

+ The sign-in is held open while you fetch it. It gives up after a few + minutes so the browser is not pinned. +

+
+ )} +