feat: convert change-email from modal to standalone page + Stripe customer.updated webhook
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 1m8s

- Add /change-email as a proper standalone page (public/src/change-email.html)
  with API key input, new email input, verification code flow, and success state
- Update footer partial: change "/#change-email" link to "/change-email" on all pages
- Remove email change modal HTML and hash-handler JS from index page source
- Add /change-email to sitemap.xml
- Rebuild all HTML files via build-html.cjs

- Add updateEmailByCustomer() to src/services/keys.ts
- Add customer.updated webhook handler in src/routes/billing.ts
  to sync email changes made via Stripe dashboard back to DocFast
This commit is contained in:
DocFast Bot 2026-02-17 11:31:37 +00:00
parent 5099bae41f
commit 8f3b1a9660
12 changed files with 674 additions and 121 deletions

View file

@ -1,6 +1,6 @@
import { Router, Request, Response } from "express";
import Stripe from "stripe";
import { createProKey, downgradeByCustomer } from "../services/keys.js";
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
import logger from "../services/logger.js";
function escapeHtml(s: string): string {
@ -210,6 +210,18 @@ router.post("/webhook", async (req: Request, res: Response) => {
logger.info({ customerId }, "customer.subscription.deleted: downgraded key to free tier");
break;
}
case "customer.updated": {
const customer = event.data.object as Stripe.Customer;
const customerId = customer.id;
const newEmail = customer.email;
if (customerId && newEmail) {
const updated = await updateEmailByCustomer(customerId, newEmail);
if (updated) {
logger.info({ customerId, newEmail }, "Customer email synced from Stripe");
}
}
break;
}
default:
break;
}

View file

@ -129,3 +129,11 @@ export async function updateKeyEmail(apiKey: string, newEmail: string): Promise<
await pool.query("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
return true;
}
export async function updateEmailByCustomer(stripeCustomerId: string, newEmail: string): Promise<boolean> {
const entry = keysCache.find(k => k.stripeCustomerId === stripeCustomerId);
if (!entry) return false;
entry.email = newEmail;
await pool.query(UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2, [newEmail, stripeCustomerId]);
return true;
}