chore(prod): real leaderboard, prod guards, payment hardening
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m4s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 2m11s

Production-readiness pass — remove mock-in-prod and harden the server:
- leaderboard: new DB-backed LeaderboardService + /api/leaderboard (ranked by
  rating, 30s cache, bounded scan); client now calls it instead of mock fake data.
- online count: client uses real /api/stats/online (dropped the fabricated ≥50 floor).
- boot guards (Production): refuse to start if Sms:ApiKey is missing (OTP would
  run in dev mode = fixed code for any phone) or Iab:AllowUnverified is true
  (forged tokens could mint coins).
- payments: ZarinPal + IAB HttpClients get 15s timeouts; ZarinPal/FlatPay gateway
  failures are now logged instead of silently swallowed.
- OTP: periodic prune of expired codes + stale rate-limit logs (was an unbounded
  in-memory leak over a long-running process).
- DB: EnableRetryOnFailure for Postgres (transient-fault resilience).
- docker-compose: ZarinPal sandbox now defaults to false (real payments).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-17 09:03:12 +03:30
parent 4739018488
commit 0790ad6fe0
8 changed files with 150 additions and 16 deletions
+8 -6
View File
@@ -547,21 +547,23 @@ export class SignalrService implements OnlineService {
}
async getOnlineCount(): Promise<number> {
// Always show a believable floor (≥50) — never the raw small/zero real count.
const floor = await this.mock.getOnlineCount(); // drifts, min 50
// Real count from the server (no fabricated floor).
try {
const res = await fetch(`${SERVER}/api/stats/online`);
if (res.ok) {
const j = (await res.json()) as { online: number };
return Math.max(j.online ?? 0, floor);
return Math.max(0, j.online ?? 0);
}
} catch {
/* fall through */
/* server unreachable */
}
return floor;
return 0;
}
getLeaderboard(): Promise<LeaderboardEntry[]> { return this.mock.getLeaderboard(); }
async getLeaderboard(): Promise<LeaderboardEntry[]> {
// Real, server-ranked leaderboard.
return this.getJson<LeaderboardEntry[]>("/api/leaderboard");
}
// shop catalog stays client-side; the purchase is server-authoritative
getShopItems(): Promise<ShopItem[]> { return this.mock.getShopItems(); }