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:
DocFast Bot 2026-02-15 10:18:25 +00:00
parent bb1881af61
commit e9d16bf2a3
13 changed files with 395 additions and 198 deletions

View file

@ -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,