fix: add /change-email route in index.ts + fix SQL query escaping in keys.ts
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 1m36s
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 1m36s
- Register GET /change-email route in src/index.ts (serves change-email.html) - Fix updateKeyEmail() SQL query string (dollar signs were stripped by heredoc) - Fix updateEmailByCustomer() SQL query string - Rebuild TypeScript dist/
This commit is contained in:
parent
8f3b1a9660
commit
1702abdeb8
9 changed files with 172 additions and 95 deletions
4
dist/index.js
vendored
4
dist/index.js
vendored
|
|
@ -196,6 +196,10 @@ app.get("/terms", (_req, res) => {
|
|||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.sendFile(path.join(__dirname, "../public/terms.html"));
|
||||
});
|
||||
app.get("/change-email", (_req, res) => {
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.sendFile(path.join(__dirname, "../public/change-email.html"));
|
||||
});
|
||||
app.get("/status", (_req, res) => {
|
||||
res.setHeader("Cache-Control", "public, max-age=60");
|
||||
res.sendFile(path.join(__dirname, "../public/status.html"));
|
||||
|
|
|
|||
61
dist/routes/billing.js
vendored
61
dist/routes/billing.js
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import { Router } from "express";
|
||||
import Stripe from "stripe";
|
||||
import { createProKey, revokeByCustomer } from "../services/keys.js";
|
||||
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
|
||||
import logger from "../services/logger.js";
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
|
|
@ -18,6 +18,27 @@ function getStripe() {
|
|||
const router = Router();
|
||||
// Track provisioned session IDs to prevent duplicate key creation
|
||||
const provisionedSessions = new Set();
|
||||
const DOCFAST_PRODUCT_ID = "prod_TygeG8tQPtEAdE";
|
||||
// Returns true if the given Stripe subscription contains a DocFast product.
|
||||
// Used to filter webhook events — this Stripe account is shared with other projects.
|
||||
async function isDocFastSubscription(subscriptionId) {
|
||||
try {
|
||||
const sub = await getStripe().subscriptions.retrieve(subscriptionId, {
|
||||
expand: ["items.data.price.product"],
|
||||
});
|
||||
return sub.items.data.some((item) => {
|
||||
const price = item.price;
|
||||
const productId = typeof price?.product === "string"
|
||||
? price.product
|
||||
: price?.product?.id;
|
||||
return productId === DOCFAST_PRODUCT_ID;
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger.error({ err, subscriptionId }, "isDocFastSubscription: failed to retrieve subscription");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Create a Stripe Checkout session for Pro subscription
|
||||
router.post("/checkout", async (_req, res) => {
|
||||
try {
|
||||
|
|
@ -114,7 +135,6 @@ router.post("/webhook", async (req, res) => {
|
|||
const customerId = session.customer;
|
||||
const email = session.customer_details?.email;
|
||||
// Filter by product — this Stripe account is shared with other projects
|
||||
const DOCFAST_PRODUCT_ID = "prod_TygeG8tQPtEAdE";
|
||||
try {
|
||||
const fullSession = await getStripe().checkout.sessions.retrieve(session.id, {
|
||||
expand: ["line_items"],
|
||||
|
|
@ -143,11 +163,44 @@ router.post("/webhook", async (req, res) => {
|
|||
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
const sub = event.data.object;
|
||||
const customerId = sub.customer;
|
||||
const shouldDowngrade = sub.status === "canceled" ||
|
||||
sub.status === "past_due" ||
|
||||
sub.status === "unpaid" ||
|
||||
sub.cancel_at_period_end === true;
|
||||
if (shouldDowngrade) {
|
||||
if (!(await isDocFastSubscription(sub.id))) {
|
||||
logger.info({ subscriptionId: sub.id }, "customer.subscription.updated: ignoring event for different product");
|
||||
break;
|
||||
}
|
||||
await downgradeByCustomer(customerId);
|
||||
logger.info({ customerId, status: sub.status, cancelAtPeriodEnd: sub.cancel_at_period_end }, "customer.subscription.updated: downgraded key to free tier");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.deleted": {
|
||||
const sub = event.data.object;
|
||||
const customerId = sub.customer;
|
||||
await revokeByCustomer(customerId);
|
||||
logger.info({ customerId }, "Subscription cancelled, key revoked");
|
||||
if (!(await isDocFastSubscription(sub.id))) {
|
||||
logger.info({ subscriptionId: sub.id }, "customer.subscription.deleted: ignoring event for different product");
|
||||
break;
|
||||
}
|
||||
await downgradeByCustomer(customerId);
|
||||
logger.info({ customerId }, "customer.subscription.deleted: downgraded key to free tier");
|
||||
break;
|
||||
}
|
||||
case "customer.updated": {
|
||||
const customer = event.data.object;
|
||||
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:
|
||||
|
|
|
|||
19
dist/services/keys.js
vendored
19
dist/services/keys.js
vendored
|
|
@ -77,12 +77,11 @@ export async function createProKey(email, stripeCustomerId) {
|
|||
keysCache.push(entry);
|
||||
return entry;
|
||||
}
|
||||
export async function revokeByCustomer(stripeCustomerId) {
|
||||
const idx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (idx >= 0) {
|
||||
const key = keysCache[idx].key;
|
||||
keysCache.splice(idx, 1);
|
||||
await pool.query("DELETE FROM api_keys WHERE key = $1", [key]);
|
||||
export async function downgradeByCustomer(stripeCustomerId) {
|
||||
const entry = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (entry) {
|
||||
entry.tier = "free";
|
||||
await pool.query("UPDATE api_keys SET tier = 'free' WHERE stripe_customer_id = $1", [stripeCustomerId]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -98,3 +97,11 @@ export async function updateKeyEmail(apiKey, newEmail) {
|
|||
await pool.query("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
|
||||
return true;
|
||||
}
|
||||
export async function updateEmailByCustomer(stripeCustomerId, newEmail) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue