File was being uploaded twice:
1. POST /parse on file select (for form auto-fill)
2. POST /upload on submit
Remove the parse network call entirely. Server already parses the APK/IPA
buffer as part of the upload handler. User fills form fields manually.
Single upload, single public-internet transfer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the APK/IPA upload required two full public-internet transfers:
1. POST /parse → browser → gateway → admin-service (full file, for metadata)
2. PUT presigned → browser → oss.gogenex.com / MinIO (full file, to store)
Now follows the same pattern as RWADurian admin-service:
- Single multipart POST /admin/versions/upload
- admin-service parses buffer in-memory (yauzl / unzipper)
- Saves to local disk (UPLOAD_DIR env, default ./uploads)
- Download served via existing GET /app/version/download/:id (streams local file)
Changes:
- file-storage.service.ts: drop minio dep, use fs/promises + crypto
- admin-version.controller.ts: POST upload now accepts multipart file,
removes GET presigned-url endpoint (no longer needed)
- version.repository.ts (frontend): single FormData POST, removes
three-step presigned-URL flow
Result: file crosses public internet once instead of twice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kong container uses network_mode:host so it shares the gateway's
network namespace and can reach 192.168.1.222:PORT directly.
Listen on 127.0.0.1:48080 (local only, Nginx proxies externally).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- infrastructure/kong/: Kong declarative config for gateway server
All service URLs use http://192.168.1.222:PORT (internal server)
admin-service gets extended timeouts (300s) for large uploads
- docker-compose.yml: admin-service uses MINIO_ENDPOINT=192.168.1.200:9200
Plain HTTP via Nginx internal proxy (no SSL, no extra_hosts needed)
New upload path:
Browser → Nginx:443 → Kong:48080 (local) → admin-service(LAN) → MinIO:9200(local)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Container maps oss.gogenex.com → 192.168.1.200 (LAN IP) so it
connects to Nginx:443 which proxies to localhost:9100 (MinIO).
Port 443 is already open in UFW; avoids hairpin NAT and raw iptables
drop rules that block direct access to 192.168.1.200:9100.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
admin-service runs on 192.168.1.222 (LAN). Connecting to MinIO via
public IP 154.84.135.121 fails with ETIMEDOUT (hairpin NAT). Use
internal gateway IP 192.168.1.200:9100 (no SSL) for S3 API calls.
Public download URLs still use https://oss.gogenex.com.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause was proxy_request_buffering off in Nginx gateway (already removed).
Kong should use default settings, matching IT0 reference architecture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add KONG_NGINX_PROXY_PROXY_REQUEST_BUFFERING=off so Kong streams
the request body to admin-service without buffering to disk.
Root cause: Nginx streams (proxy_request_buffering off) → Kong buffers
to disk → Kong returns 400 with empty body when forwarding to upstream.
Fix: Kong also streams, matching Nginx's streaming behavior end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend wraps data in extra layer:
system-health → {code:0, data:{services:[...]}}
realtime-trades → {code:0, data:{items:[...], total:N}}
HttpClient strips outer data but leaves inner object.
Fix: type as {services/items} and access nested arrays.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth.store: eslint-disable with explicit comment for intentional infra access
(session orchestration is a designated cross-layer boundary)
- Add auth.use-cases.ts (LoginUseCase / LogoutUseCase) for use by views/hooks
- Fix no-explicit-any in AppVersionManagementPage (use unknown + type assertion)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instance-level default Content-Type: application/json was overriding
browser's auto-generated multipart/form-data boundary. Remove it for
FormData so browser sets correct Content-Type with boundary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without boundary multer receives undefined file. Also add guards in
backend parse/upload to avoid crash if file is missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Prevent TypeError if useApi returns non-array shape
- Add HttpClient.get logging to trace raw vs unwrapped response
- Parse timeout: 120s → 300s (matches upload, avoids timeout on large files)
- Show hint for large files (>30MB) during parse
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth.store: persist refreshToken alongside accessToken
- http.client: on 401, auto-refresh token and retry original request
with mutex lock to prevent concurrent refresh calls; only redirect
to /login if refresh itself fails
- upload modal: restore auto-parse on file select; show warning if
parse fails; add console logs for debugging; fix button disabled
during parsing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove auto-parse on file select (was uploading 48MB twice, took 100+ sec)
- Backend /upload already parses APK internally, version fields are now optional
- Show file name + size after selection
- Show progress hint during upload
- Better error extraction from API response
- Clear error when new file is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manually setting Content-Type: multipart/form-data without the boundary
causes the server to reject the request. Axios automatically sets the
correct header with boundary when FormData is passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ManifestParser(buffer) 内部自带 BinaryXmlParser,无需先调用 BinaryXmlParser.parse()
再把结果传入 ManifestParser,否则导致 readUInt16LE is not a function。
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Flutter wallet_provider.dart calls /api/v1/wallet/balance but the
controller only had GET /wallet (root). Adding /wallet/balance alias.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 6 service strategies were returning { sub } but controllers use req.user.id.
Change return value from { sub: payload.sub } to { id: payload.sub } so that
req.user.id resolves correctly in all protected controllers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
notification-service was missing JWT_ACCESS_SECRET, causing it to use the
fallback 'dev-access-secret' instead of 'dev-access-secret-change-in-production'.
This made all auth-protected endpoints return 401, triggering logout in the app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flutter_local_notifications requires coreLibraryDesugaringEnabled = true
and the desugaring-api dependency to build on Android.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- credit_page.dart: remove const from Padding containing context.t() call
- issuer_coupon_service/redemption_service/issuer_finance_service:
cast inner['total'] to int? to match named record return type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>