fix(keys): add DB fallback to updateEmailByCustomer, updateKeyEmail, and recover route (BUG-108, BUG-109, BUG-110)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m8s

- updateEmailByCustomer: DB fallback when stripe_customer_id not in cache
- updateKeyEmail: DB fallback when key not in cache
- POST /v1/recover: DB fallback when email not in cache (was only on verify)
- 6 TDD tests added (keys-email-update.test.ts, recover-initial-db-fallback.test.ts)
- 547 tests total, all passing
This commit is contained in:
DocFast CEO 2026-03-07 20:06:13 +01:00
parent 424a16ed8a
commit d376d586fe
4 changed files with 286 additions and 4 deletions

View file

@ -186,16 +186,72 @@ export function getAllKeys(): ApiKey[] {
export async function updateKeyEmail(apiKey: string, newEmail: string): Promise<boolean> {
const entry = keysCache.find((k) => k.key === apiKey);
if (!entry) return false;
entry.email = newEmail;
if (entry) {
entry.email = newEmail;
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
return true;
}
// DB fallback: key may exist on another pod's cache or after a restart
logger.info({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: cache miss, falling back to DB");
const result = await queryWithRetry(
"SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE key = $1 LIMIT 1",
[apiKey]
);
if (result.rows.length === 0) {
logger.warn({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: key not found in cache or DB");
return false;
}
const row = result.rows[0];
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
// Hydrate local cache
const cached: ApiKey = {
key: row.key,
tier: row.tier as "free" | "pro",
email: newEmail,
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
stripeCustomerId: row.stripe_customer_id || undefined,
};
keysCache.push(cached);
logger.info({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: updated via DB fallback");
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;
if (entry) {
entry.email = newEmail;
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]);
return true;
}
// DB fallback: key may exist on another pod's cache or after a restart
logger.info({ stripeCustomerId }, "updateEmailByCustomer: cache miss, falling back to DB");
const result = await queryWithRetry(
"SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE stripe_customer_id = $1 LIMIT 1",
[stripeCustomerId]
);
if (result.rows.length === 0) {
logger.warn({ stripeCustomerId }, "updateEmailByCustomer: customer not found in cache or DB");
return false;
}
const row = result.rows[0];
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]);
// Hydrate local cache
const cached: ApiKey = {
key: row.key,
tier: row.tier as "free" | "pro",
email: newEmail,
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
stripeCustomerId: row.stripe_customer_id || undefined,
};
keysCache.push(cached);
logger.info({ stripeCustomerId, key: row.key }, "updateEmailByCustomer: updated via DB fallback");
return true;
}