Account management
Account management flows often need mailbox verification too. A common example is changing the email address on an existing account: create a challenge for the new mailbox, verify the code, then update the account.
Start the email-change challenge
Section titled “Start the email-change challenge”import { createMailboxChallenge } from "@passlock/server";
const user = await requireSignedInUser();const newEmail = "new-address@example.com";
const result = await createMailboxChallenge({ tenancyId: "myTenancyId", apiKey: "myApiKey", email: newEmail, purpose: "email-change", userId: String(user.id), invalidateOthers: true,});
// message contains rendered HTML and plain-text email content.// result.challenge.code is also available if you prefer to render your own email.const { challengeId, secret, message, email } = result.challenge;
// save this in the user's session or a secure HTTP only cookieawait savePendingEmailChange({ challengeId, secret, userId: user.id, email,});
await sendCodeEmail({ email, message });import { createMailboxChallenge, isChallengeRateLimitedError,} from "@passlock/server/safe";
const user = await requireSignedInUser();const newEmail = "new-address@example.com";
const result = await createMailboxChallenge({ tenancyId: "myTenancyId", apiKey: "myApiKey", email: newEmail, purpose: "email-change", userId: String(user.id), invalidateOthers: true,});
if (result.success) { // message contains rendered HTML and plain-text email content. // result.value.challenge.code is also available if you prefer to render your own email. const { challengeId, secret, message, email } = result.value.challenge;
// save this in the user's session or a secure HTTP only cookie await savePendingEmailChange({ challengeId, secret, userId: user.id, email, });
await sendCodeEmail({ email, message });} else if (isChallengeRateLimitedError(result.error)) { return showRateLimit(result.error.retryAfterSeconds);} else { throw new Error(result.error.message);}Verify the new mailbox and update the account
Section titled “Verify the new mailbox and update the account”import { verifyMailboxChallenge } from "@passlock/server";
const user = await requireSignedInUser();
// fetch from the user's session or secure cookieconst pending = await loadPendingEmailChange(user.id);
const verified = await verifyMailboxChallenge({ tenancyId: "myTenancyId", apiKey: "myApiKey", challengeId: pending.challengeId, secret: pending.secret, code: form.code,});
if (verified.challenge.purpose !== "email-change") { throw new Error("Unexpected challenge purpose");}
if (verified.challenge.userId !== String(user.id)) { throw new Error("Unexpected user");}
await updateUserEmail(user.id, verified.challenge.email);await clearPendingEmailChange(user.id);await sendEmailChangeNotice(user.email);import { verifyMailboxChallenge } from "@passlock/server/safe";
const user = await requireSignedInUser();
// fetch from the user's session or secure cookieconst pending = await loadPendingEmailChange(user.id);
const result = await verifyMailboxChallenge({ tenancyId: "myTenancyId", apiKey: "myApiKey", challengeId: pending.challengeId, secret: pending.secret, code: form.code,});
if (result.success) { const { challenge } = result.value;
if (challenge.purpose !== "email-change") { throw new Error("Unexpected challenge purpose"); }
if (challenge.userId !== String(user.id)) { throw new Error("Unexpected user"); }
await updateUserEmail(user.id, challenge.email); await clearPendingEmailChange(user.id); await sendEmailChangeNotice(user.email);} else { return showVerificationError(result.error);}The same endpoints are available over raw HTTP if you are not using @passlock/server. See the REST API mailbox challenge reference.