Migrate from JSON to PostgreSQL, update SLA to 99.5%
- Replace JSON file storage with PostgreSQL (pg package) - Add db.ts service for connection pool and schema init - Rewrite keys.ts, verification.ts, usage.ts for async PostgreSQL - Update all routes for async function signatures - Add migration script (scripts/migrate-to-postgres.mjs) - Update docker-compose.yml with DATABASE_* env vars - Change SLA from 99.9% to 99.5% in landing page
This commit is contained in:
parent
bb1881af61
commit
e9d16bf2a3
13 changed files with 395 additions and 198 deletions
|
|
@ -56,7 +56,7 @@ router.get("/success", async (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const keyInfo = createProKey(email, customerId);
|
||||
const keyInfo = await createProKey(email, customerId);
|
||||
|
||||
// Return a nice HTML page instead of raw JSON
|
||||
res.send(`<!DOCTYPE html>
|
||||
|
|
@ -125,14 +125,14 @@ router.post("/webhook", async (req: Request, res: Response) => {
|
|||
break;
|
||||
}
|
||||
|
||||
const keyInfo = createProKey(email, customerId);
|
||||
const keyInfo = await createProKey(email, customerId);
|
||||
console.log(`checkout.session.completed: provisioned pro key for ${email} (customer: ${customerId}, key: ${keyInfo.key.slice(0, 12)}...)`);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.deleted": {
|
||||
const sub = event.data.object as Stripe.Subscription;
|
||||
const customerId = sub.customer as string;
|
||||
revokeByCustomer(customerId);
|
||||
await revokeByCustomer(customerId);
|
||||
console.log(`Subscription cancelled for ${customerId}, key revoked`);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const changeLimiter = rateLimit({
|
|||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// Step 1: Request email change — sends verification code to NEW email
|
||||
router.post("/", changeLimiter, async (req: Request, res: Response) => {
|
||||
const apiKey = req.headers.authorization?.replace(/^Bearer\s+/i, "") || req.body?.apiKey;
|
||||
const newEmail = req.body?.newEmail;
|
||||
|
|
@ -44,8 +43,7 @@ router.post("/", changeLimiter, async (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const pending = createPendingVerification(cleanEmail);
|
||||
(pending as any)._changeContext = { apiKey, newEmail: cleanEmail, oldEmail: userKey.email };
|
||||
const pending = await createPendingVerification(cleanEmail);
|
||||
|
||||
sendVerificationEmail(cleanEmail, (pending as any).code).catch((err: Error) => {
|
||||
console.error(`Failed to send email change verification to ${cleanEmail}:`, err);
|
||||
|
|
@ -54,7 +52,6 @@ router.post("/", changeLimiter, async (req: Request, res: Response) => {
|
|||
res.json({ status: "verification_sent", message: "Verification code sent to your new email address." });
|
||||
});
|
||||
|
||||
// Step 2: Verify code — updates email
|
||||
router.post("/verify", changeLimiter, async (req: Request, res: Response) => {
|
||||
const apiKey = req.headers.authorization?.replace(/^Bearer\s+/i, "") || req.body?.apiKey;
|
||||
const { newEmail, code } = req.body || {};
|
||||
|
|
@ -74,11 +71,11 @@ router.post("/verify", changeLimiter, async (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const result = verifyCode(cleanEmail, cleanCode);
|
||||
const result = await verifyCode(cleanEmail, cleanCode);
|
||||
|
||||
switch (result.status) {
|
||||
case "ok": {
|
||||
const updated = updateKeyEmail(apiKey, cleanEmail);
|
||||
const updated = await updateKeyEmail(apiKey, cleanEmail);
|
||||
if (updated) {
|
||||
res.json({ status: "updated", message: "Email address updated successfully.", newEmail: cleanEmail });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ const recoverLimiter = rateLimit({
|
|||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// Step 1: Request recovery — sends verification code via email
|
||||
router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
||||
const { email } = req.body || {};
|
||||
|
||||
|
|
@ -24,20 +23,16 @@ router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
|||
}
|
||||
|
||||
const cleanEmail = email.trim().toLowerCase();
|
||||
|
||||
// Check if this email has any keys
|
||||
const keys = getAllKeys();
|
||||
const userKey = keys.find(k => k.email === cleanEmail);
|
||||
|
||||
// Always return success to prevent email enumeration
|
||||
if (!userKey) {
|
||||
res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." });
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = createPendingVerification(cleanEmail);
|
||||
const pending = await createPendingVerification(cleanEmail);
|
||||
|
||||
// Send verification CODE only — NEVER send the API key via email
|
||||
sendVerificationEmail(cleanEmail, pending.code).catch(err => {
|
||||
console.error(`Failed to send recovery email to ${cleanEmail}:`, err);
|
||||
});
|
||||
|
|
@ -45,7 +40,6 @@ router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
|||
res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." });
|
||||
});
|
||||
|
||||
// Step 2: Verify code — returns API key in response (NEVER via email)
|
||||
router.post("/verify", recoverLimiter, async (req: Request, res: Response) => {
|
||||
const { email, code } = req.body || {};
|
||||
|
||||
|
|
@ -57,7 +51,7 @@ router.post("/verify", recoverLimiter, async (req: Request, res: Response) => {
|
|||
const cleanEmail = email.trim().toLowerCase();
|
||||
const cleanCode = String(code).trim();
|
||||
|
||||
const result = verifyCode(cleanEmail, cleanCode);
|
||||
const result = await verifyCode(cleanEmail, cleanCode);
|
||||
|
||||
switch (result.status) {
|
||||
case "ok": {
|
||||
|
|
@ -65,7 +59,6 @@ router.post("/verify", recoverLimiter, async (req: Request, res: Response) => {
|
|||
const userKey = keys.find(k => k.email === cleanEmail);
|
||||
|
||||
if (userKey) {
|
||||
// Return key in response — shown once in browser, never emailed
|
||||
res.json({
|
||||
status: "recovered",
|
||||
apiKey: userKey.key,
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ const verifyLimiter = rateLimit({
|
|||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
function rejectDuplicateEmail(req: Request, res: Response, next: Function) {
|
||||
async function rejectDuplicateEmail(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body || {};
|
||||
if (email && typeof email === "string") {
|
||||
const cleanEmail = email.trim().toLowerCase();
|
||||
if (isEmailVerified(cleanEmail)) {
|
||||
if (await isEmailVerified(cleanEmail)) {
|
||||
res.status(409).json({ error: "Email already registered" });
|
||||
return;
|
||||
}
|
||||
|
|
@ -45,14 +45,13 @@ router.post("/free", rejectDuplicateEmail, signupLimiter, async (req: Request, r
|
|||
|
||||
const cleanEmail = email.trim().toLowerCase();
|
||||
|
||||
if (isEmailVerified(cleanEmail)) {
|
||||
if (await isEmailVerified(cleanEmail)) {
|
||||
res.status(409).json({ error: "This email is already registered. Contact support if you need help." });
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = createPendingVerification(cleanEmail);
|
||||
const pending = await createPendingVerification(cleanEmail);
|
||||
|
||||
// Send verification code via email (fire-and-forget, don't block response)
|
||||
sendVerificationEmail(cleanEmail, pending.code).catch(err => {
|
||||
console.error(`Failed to send verification email to ${cleanEmail}:`, err);
|
||||
});
|
||||
|
|
@ -64,7 +63,7 @@ router.post("/free", rejectDuplicateEmail, signupLimiter, async (req: Request, r
|
|||
});
|
||||
|
||||
// Step 2: Verify code — creates API key
|
||||
router.post("/verify", verifyLimiter, (req: Request, res: Response) => {
|
||||
router.post("/verify", verifyLimiter, async (req: Request, res: Response) => {
|
||||
const { email, code } = req.body || {};
|
||||
|
||||
if (!email || !code) {
|
||||
|
|
@ -75,17 +74,17 @@ router.post("/verify", verifyLimiter, (req: Request, res: Response) => {
|
|||
const cleanEmail = email.trim().toLowerCase();
|
||||
const cleanCode = String(code).trim();
|
||||
|
||||
if (isEmailVerified(cleanEmail)) {
|
||||
if (await isEmailVerified(cleanEmail)) {
|
||||
res.status(409).json({ error: "This email is already verified." });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = verifyCode(cleanEmail, cleanCode);
|
||||
const result = await verifyCode(cleanEmail, cleanCode);
|
||||
|
||||
switch (result.status) {
|
||||
case "ok": {
|
||||
const keyInfo = createFreeKey(cleanEmail);
|
||||
const verification = createVerification(cleanEmail, keyInfo.key);
|
||||
const keyInfo = await createFreeKey(cleanEmail);
|
||||
const verification = await createVerification(cleanEmail, keyInfo.key);
|
||||
verification.verifiedAt = new Date().toISOString();
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue