Phase 2, email verification rework, ui stuff

This commit is contained in:
binwiederhier
2026-06-12 11:40:59 -04:00
parent 44dac47d76
commit 30dd4840a2
19 changed files with 789 additions and 332 deletions
+38 -8
View File
@@ -4,6 +4,8 @@ import {
accountBillingSubscriptionUrl,
accountEmailUrl,
accountEmailVerifyUrl,
accountEmailPrimaryUrl,
accountEmailResendUrl,
accountPasswordUrl,
accountPhoneUrl,
accountPhoneVerifyUrl,
@@ -342,9 +344,11 @@ class AccountApi {
});
}
async verifyEmail(email) {
const url = accountEmailVerifyUrl(config.base_url);
console.log(`[AccountApi] Sending email verification ${url}`);
// startEmailVerification begins adding an email: the server stores a pending verification and
// emails a magic link. The address is not verified until the link is clicked.
async startEmailVerification(email) {
const url = accountEmailUrl(config.base_url);
console.log(`[AccountApi] Starting email verification ${url}`);
await fetchOrThrow(url, {
method: "PUT",
headers: withBearerAuth({}, session.token()),
@@ -354,15 +358,41 @@ class AccountApi {
});
}
async addEmail(email, code) {
const url = accountEmailUrl(config.base_url);
console.log(`[AccountApi] Adding email with verification code ${url}`);
// verifyEmailToken performs verification from the magic-link landing page. It is unauthenticated:
// the token identifies the account, so this works even when clicked from a logged-out browser.
async verifyEmailToken(token) {
const url = accountEmailVerifyUrl(config.base_url);
console.log(`[AccountApi] Verifying email token ${url}`);
await fetchOrThrow(url, {
method: "PUT",
method: "POST",
body: JSON.stringify({
token,
}),
});
}
// resendEmailVerification re-sends the magic link for a pending (unverified) address.
async resendEmailVerification(email) {
const url = accountEmailResendUrl(config.base_url);
console.log(`[AccountApi] Resending email verification ${url}`);
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
email,
}),
});
}
// setPrimaryEmail marks an already-verified address as the primary (recovery) email.
async setPrimaryEmail(email) {
const url = accountEmailPrimaryUrl(config.base_url);
console.log(`[AccountApi] Setting primary email ${url}`);
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
email,
code,
}),
});
}
+11 -1
View File
@@ -51,7 +51,15 @@ export class EmailVerificationCodeInvalidError extends Error {
static CODE = 40051; // errHTTPBadRequestEmailVerificationCodeInvalid
constructor() {
super("Email verification code invalid or expired");
super("Email verification link invalid or expired");
}
}
export class EmailPrimaryElsewhereError extends Error {
static CODE = 40908; // errHTTPConflictEmailPrimaryElsewhere
constructor() {
super("Email address is the recovery email on another account");
}
}
@@ -73,6 +81,8 @@ export const throwAppError = async (response) => {
throw new IncorrectPasswordError();
} else if (error.code === EmailVerificationCodeInvalidError.CODE) {
throw new EmailVerificationCodeInvalidError();
} else if (error.code === EmailPrimaryElsewhereError.CODE) {
throw new EmailPrimaryElsewhereError();
} else if (error?.error) {
throw new Error(`Error ${error.code}: ${error.error}`);
}
+2
View File
@@ -35,6 +35,8 @@ export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
export const accountEmailUrl = (baseUrl) => `${baseUrl}/v1/account/email`;
export const accountEmailVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/email/verify`;
export const accountEmailPrimaryUrl = (baseUrl) => `${baseUrl}/v1/account/email/primary`;
export const accountEmailResendUrl = (baseUrl) => `${baseUrl}/v1/account/email/resend`;
export const validUrl = (url) => url.match(/^https?:\/\/.+/);