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>
ZarinPal only accepts callbacks on pay.flatrender.ir, so bargevasat
pays through the shared broker and is credited via a signed webhook.
- FlatPayService: broker client (HMAC-signed /v1/pay/request) + webhook
signature verification + in-memory idempotency guard.
- Program.cs: /api/coins/pay/request prefers the broker when configured
(FlatPay__ApiKey/Secret set), else the legacy direct ZarinPal path;
new public POST /api/coins/pay/webhook verifies the HMAC and credits
coins from the echoed metadata (idempotent).
- appsettings + docker-compose: FlatPay config (empty ⇒ legacy path).
- web: recognise the broker's ?status=Paid return + re-refresh profile
(coins are credited server-side via webhook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OtpService: a designated test phone (default 09120000000 / code 453115,
overridable via Sms__TestPhone/Sms__TestCode) skips real SMS and always
verifies — for Google Play / Bazaar / Myket reviewers. Give them these creds.
- Matchmaking UX: tapping a league now navigates to the matchmaking screen
BEFORE awaiting the SignalR handshake, so the button can't freeze. Added a
watchdog hint after 28s ("connection took too long, cancel & retry") so it
never spins forever when the hub doesn't connect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OtpService: generates a 5-digit code, stores it (in-memory, 120s TTL, max 5
tries, single-use), and sends it via Kavenegar verify/lookup
(template "hokmotp", %token = code). Normalizes +98/98 → 09xxxxxxxxx.
- /api/auth/otp/request + /verify now use it. No SMS_API_KEY ⇒ dev mode
(accepts a fixed code, returns devCode for local testing).
- Config: Sms section (appsettings) + Sms__* compose mapping + SMS_* in the
ENV_FILE template.
Security: sanitized deploy/ENV_FILE.example back to placeholders (it had picked
up real secrets) and added /deploy/ENV_FILE.local to .gitignore as the real
master copy (never committed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docker compose build interpolates the whole file, so the ${JWT_KEY:?} guard
failed the build step when ENV_FILE lacked JWT_KEY. Default it empty (${JWT_KEY:-})
so build/db steps succeed, and enforce the secret at runtime instead: the server
throws on boot in Production if Jwt:Key is missing/dev/<32 chars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The HTTPS Nexus serves an incomplete cert chain that container trust stores
reject (NU1301 PartialChain / UNABLE_TO_GET_ISSUER), failing CI restore/install.
- NuGet has no strict-ssl flag → point CI + Dockerfile + compose at the plain-HTTP
Nexus (http://171.22.25.73:8081, allowInsecureConnections) — no TLS, no cert check.
- npm: add --strict-ssl=false to the CI web-check install (Dockerfile already had it);
Docker npm registry default also moved to the HTTP Nexus.
- ENV_FILE.example documents NUGET_INDEX/NPM_REGISTRY overrides.
Local dev (Windows trusts the cert) + image base pulls (Docker trusts it) are
unaffected — only in-container package feeds switch to HTTP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Issues found bringing the stack up locally and fixed:
- Server was loopback-only inside the container (appsettings "Urls=localhost"
wins over ASPNETCORE_URLS) → published port returned "empty reply". Force the
bind with command-line args: ENTRYPOINT dotnet Hokm.Server.dll --urls 0.0.0.0:5005.
- Web image: npm install crashed on alpine ("Exit handler never called"); root
cause was UNABLE_TO_GET_ISSUER_CERT_LOCALLY — the Nexus mirror serves a partial
chain that Node's CA bundle can't complete. Use npm ci + strict-ssl=false.
- .NET restore hit the same partial chain (NU1301 PartialChain). Both registries
are now build ARGs (NUGET_INDEX / NPM_REGISTRY) defaulting to the HTTPS mirror
(CI runner trusts it); local .env overrides to the plain-HTTP Nexus
(http://171.22.25.73:8081) which has no TLS. NuGet feed is generated inline with
allowInsecureConnections so .NET 10 accepts the HTTP source.
Verified on local Docker (Postgres-backed): db+server+web all healthy; API + web
reachable from host on 1505/1500; auth → profile (1000 coins) → friend add/accept
(bidirectional) → chat (unread) all 200; rows persisted in Postgres
(Profiles=2, Friends=2, Messages=1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipeline (.gitea/workflows/ci-cd.yml), all images/packages via Nexus mirror:
- CI api-build: dotnet restore/build server/Hokm.slnx + run Hokm.Sim (rules).
- CI web-check: npm install + tsc --noEmit + next build (static export).
- deploy (self-hosted): pre-deploy pg_dump backup, rollback image tag, build,
bring up db -> server -> web with stop+rm+up --no-deps (no force-recreate,
no bare compose down), health-wait each, prune.
Local stack (docker-compose.yml), ports in 1500-1600 so it coexists with manual
dev on 3000/5005: web :1500 (nginx static) -> server :1505 (.NET) -> db :1510
(postgres, named volume + backups). Dockerfiles: server (.NET, NuGet via
nuget.docker.config, binds 0.0.0.0, busybox wget healthcheck) + web (Next static
export -> nginx, NEXT_PUBLIC_* baked as build args). nginx.conf SPA fallback.
Config: server CORS is now config-driven (Cors__Origins) so the deployed web
origin is allowed without code edits. deploy/ENV_FILE.example documents the
Gitea ENV_FILE secret; DEPLOY.md covers setup/run/LAN-IP/rollback/migrations.
Fonts: switch Vazirmatn + Plus Jakarta Sans from next/font/google (build-time
Google fetch -> fails on the Iran CI runner) to self-hosted @fontsource-variable
packages. Build is offline and ~3x faster; 7 woff2 emitted into out/.
Verified locally: dotnet build slnx + Hokm.Sim (300 matches, exit 0); tsc clean;
next build clean with self-hosted fonts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>