fix(BUG-106): DB fallback for downgradeByCustomer and recover route
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m7s

- downgradeByCustomer now queries DB when key not in memory cache,
  preventing cancelled customers from keeping Pro access in multi-pod setups
- recover/verify endpoint falls back to DB lookup when cache miss on email
- Added TDD tests for both fallback paths (4 new tests)
This commit is contained in:
OpenClaw 2026-03-06 20:06:04 +01:00
parent 4473641ee1
commit b964b98a8b
4 changed files with 240 additions and 2 deletions

View file

@ -3,6 +3,7 @@ import rateLimit from "express-rate-limit";
import { createPendingVerification, verifyCode } from "../services/verification.js";
import { sendVerificationEmail } from "../services/email.js";
import { getAllKeys } from "../services/keys.js";
import { queryWithRetry } from "../services/db.js";
import logger from "../services/logger.js";
const router = Router();
@ -143,8 +144,27 @@ router.post("/verify", recoverLimiter, async (req: Request, res: Response) => {
switch (result.status) {
case "ok": {
const keys = getAllKeys();
const userKey = keys.find(k => k.email === cleanEmail);
let userKey = keys.find(k => k.email === cleanEmail);
// DB fallback: cache may be stale in multi-replica setups
if (!userKey) {
logger.info({ email: cleanEmail }, "recover verify: cache miss, falling back to DB");
const dbResult = await queryWithRetry(
"SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE email = $1 LIMIT 1",
[cleanEmail]
);
if (dbResult.rows.length > 0) {
const row = dbResult.rows[0];
userKey = {
key: row.key,
tier: row.tier as "free" | "pro",
email: row.email,
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
stripeCustomerId: row.stripe_customer_id || undefined,
};
}
}
if (userKey) {
res.json({
status: "recovered",