Skip to content

fix(throttler): rate-limit per real client IP instead of proxy contai…#4

Open
0xaldric wants to merge 1 commit into
mainfrom
feat/ip-rate-limit
Open

fix(throttler): rate-limit per real client IP instead of proxy contai…#4
0xaldric wants to merge 1 commit into
mainfrom
feat/ip-rate-limit

Conversation

@0xaldric

@0xaldric 0xaldric commented May 5, 2026

Copy link
Copy Markdown
Collaborator

…ner IP

Next.js proxy (frontend + admin) was not forwarding X-Forwarded-For to the backend, so all throttle buckets collapsed into the container IP — effectively disabling per-user rate limiting.

Changes:

  • Add src/common/utils/client-ip.util.ts (shared IP extraction utility, moved from articles module which now re-exports it)
  • Add RealIpThrottlerGuard in race-result module that overrides getTracker() to read X-Forwarded-For / X-Real-IP headers directly, matching the existing articles module approach (no global trust proxy)
  • Re-enable ThrottlerModule.forRoot in race-result.module.ts (was temporarily disabled); register RealIpThrottlerGuard as provider
  • Replace all @UseGuards(ThrottlerGuard) with RealIpThrottlerGuard in race-result.controller.ts (6 endpoints)
  • Forward x-forwarded-for header in both frontend and admin proxy routes so the real nginx-injected client IP reaches the backend

…ner IP

Next.js proxy (frontend + admin) was not forwarding X-Forwarded-For to the
backend, so all throttle buckets collapsed into the container IP — effectively
disabling per-user rate limiting.

Changes:
- Add `src/common/utils/client-ip.util.ts` (shared IP extraction utility,
  moved from articles module which now re-exports it)
- Add `RealIpThrottlerGuard` in race-result module that overrides
  `getTracker()` to read X-Forwarded-For / X-Real-IP headers directly,
  matching the existing articles module approach (no global trust proxy)
- Re-enable ThrottlerModule.forRoot in race-result.module.ts (was temporarily
  disabled); register RealIpThrottlerGuard as provider
- Replace all `@UseGuards(ThrottlerGuard)` with `RealIpThrottlerGuard` in
  race-result.controller.ts (6 endpoints)
- Forward x-forwarded-for header in both frontend and admin proxy routes so
  the real nginx-injected client IP reaches the backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
minhnb9897 added a commit that referenced this pull request May 9, 2026
PROD bugs phát hiện 2026-05-09 trên result.5bib.com (race FUYU live, athletes
cảm nhận trực tiếp). Full BUGFIX workflow protocol — Manager init → BA PRD →
Manager plan APPROVED → Coder 2 rounds → QC APPROVED WITH CAVEATS.

Bug #1 — parseDistanceKm (badge.service.ts:21-66) ăn 'm' thành miles
- "4800m" (4.8km) match (?:mi|miles|m)\b → ×1.60934 → 7724km → fire ULTRA badge
- Fix: Option B parse meters TRƯỚC miles, regex case-sensitive ngăn '100M' regression
- Test: 4800m→4.8, 5km→5, 100mi→160.93, 100M→160.93 (legacy preserved)

Bug #2 — detectPodium/AgePodium/Ultra thiếu finished guard
- Vendor RaceResult assign OverallRank=1 PRE-RACE → "🥇 Vô địch chung cuộc"
  + confetti fires cho athlete đang chạy chưa finish
- Fix: helper isValidFinisher() guard finished===1 AND parseChipTime>0 ở 3
  detect functions, wrappers pass-through finished+chipTime từ result doc
- Test: 4 cases per function (valid finisher, finished=0, chipTime="0:00", chipTime="")

Bug #3 — getCourseStats CROSS-RACE DATA LEAK
- Endpoint GET /race-results/stats/:courseId thiếu raceId → aggregation
  $match { courseId } join TẤT CẢ races platform → counter banner FUYU
  hiển thị 1.567/1.231/336 thay vì 15+57=72 (data thật)
- Fix: endpoint → /stats/:raceId/:courseId, service signature
  getCourseStats(raceId, courseId), 4 query sites filter raceId (aggregate
  + distinct nationality + bucketPipeline + firstDoc), cache key
  stats:<raceId>:<courseId>
- BREAKING: SDK regen, frontend caller update raceId, old URL 404

Bug #4 — Image URL không URL-encode → ảnh bìa + ảnh cự ly không hiển thị
- DB lưu raw filename Vietnamese: ".../FII_ÄAÌ£I HOÌ£ÌI THEÌÌ THAO_OFFICIAL KV.png"
- Browser CSS background-image: url(...) reject malformed → silent fail
- Fix: helper safeImageUrl(url) wrap encodeURI() ở 6 sites trong
  races/[slug]/page.tsx (banner, course cards, logo, avatars). Pattern
  follow precedent RaceHeroHeader.tsx:101.
- BR-DISPLAY-11 ghi convention: image render PHẢI encodeURI để handle VN diacritics
- Bug #5 (backend upload slugify) defer F-022 (root cause systemic, scope rộng)

Pre-push CI parity gate 5/5 PASS:
- Backend clean install + build + dist/main.js (3848 bytes — anti F-019 Round 4)
- Admin/Frontend builds clean
- TypeScript strict 0 errors trên touched files
- Lockfile sync 0 diff vs origin/main

Tests:
- badge.service.spec.ts: 91/91 PASS (29 F-021-specific)
- race-result.service.spec.ts: 18 PASS / 5 FAIL (pre-existing TD, NOT regression)

Files changed: 14 (8 Scope Lock + 4 SDK auto-regen + 2 downstream JUSTIFIED)

QC verdict: 🟡 APPROVED WITH CAVEATS (0 CRITICAL, 0 NEW HIGH, 100% in-scope BR coverage)
- TD-F021-PURGECACHE-PATTERN-LAG (admin purgeCache stats:* old pattern → F-022)
- 5 pre-existing test infra fails documented
- Cache flush badge:* + stats:* sau deploy: Manager run

Manager note: workflow protocol B sequential (init → PRD → plan → code 2-round → QC),
KHÔNG bypass dù P0 prod incident. Lessons từ F-019 4-round deploy fail đã apply
qua Pre-push CI parity gate convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
minhnb9897 added a commit that referenced this pull request May 9, 2026
PROD bugs phát hiện 2026-05-09 trên result.5bib.com (race FUYU live, athletes
cảm nhận trực tiếp). Full BUGFIX workflow protocol — Manager init → BA PRD →
Manager plan APPROVED → Coder 2 rounds → QC APPROVED WITH CAVEATS.

Bug #1 — parseDistanceKm (badge.service.ts:21-66) ăn 'm' thành miles
- "4800m" (4.8km) match (?:mi|miles|m)\b → ×1.60934 → 7724km → fire ULTRA badge
- Fix: Option B parse meters TRƯỚC miles, regex case-sensitive ngăn '100M' regression
- Test: 4800m→4.8, 5km→5, 100mi→160.93, 100M→160.93 (legacy preserved)

Bug #2 — detectPodium/AgePodium/Ultra thiếu finished guard
- Vendor RaceResult assign OverallRank=1 PRE-RACE → "🥇 Vô địch chung cuộc"
  + confetti fires cho athlete đang chạy chưa finish
- Fix: helper isValidFinisher() guard finished===1 AND parseChipTime>0 ở 3
  detect functions, wrappers pass-through finished+chipTime từ result doc
- Test: 4 cases per function (valid finisher, finished=0, chipTime="0:00", chipTime="")

Bug #3 — getCourseStats CROSS-RACE DATA LEAK
- Endpoint GET /race-results/stats/:courseId thiếu raceId → aggregation
  $match { courseId } join TẤT CẢ races platform → counter banner FUYU
  hiển thị 1.567/1.231/336 thay vì 15+57=72 (data thật)
- Fix: endpoint → /stats/:raceId/:courseId, service signature
  getCourseStats(raceId, courseId), 4 query sites filter raceId (aggregate
  + distinct nationality + bucketPipeline + firstDoc), cache key
  stats:<raceId>:<courseId>
- BREAKING: SDK regen, frontend caller update raceId, old URL 404

Bug #4 — Image URL không URL-encode → ảnh bìa + ảnh cự ly không hiển thị
- DB lưu raw filename Vietnamese: ".../FII_ÄAÌ£I HOÌ£ÌI THEÌÌ THAO_OFFICIAL KV.png"
- Browser CSS background-image: url(...) reject malformed → silent fail
- Fix: helper safeImageUrl(url) wrap encodeURI() ở 6 sites trong
  races/[slug]/page.tsx (banner, course cards, logo, avatars). Pattern
  follow precedent RaceHeroHeader.tsx:101.
- BR-DISPLAY-11 ghi convention: image render PHẢI encodeURI để handle VN diacritics
- Bug #5 (backend upload slugify) defer F-022 (root cause systemic, scope rộng)

Pre-push CI parity gate 5/5 PASS:
- Backend clean install + build + dist/main.js (3848 bytes — anti F-019 Round 4)
- Admin/Frontend builds clean
- TypeScript strict 0 errors trên touched files
- Lockfile sync 0 diff vs origin/main

Tests:
- badge.service.spec.ts: 91/91 PASS (29 F-021-specific)
- race-result.service.spec.ts: 18 PASS / 5 FAIL (pre-existing TD, NOT regression)

Files changed: 14 (8 Scope Lock + 4 SDK auto-regen + 2 downstream JUSTIFIED)

QC verdict: 🟡 APPROVED WITH CAVEATS (0 CRITICAL, 0 NEW HIGH, 100% in-scope BR coverage)
- TD-F021-PURGECACHE-PATTERN-LAG (admin purgeCache stats:* old pattern → F-022)
- 5 pre-existing test infra fails documented
- Cache flush badge:* + stats:* sau deploy: Manager run

Manager note: workflow protocol B sequential (init → PRD → plan → code 2-round → QC),
KHÔNG bypass dù P0 prod incident. Lessons từ F-019 4-round deploy fail đã apply
qua Pre-push CI parity gate convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
minhnb9897 added a commit that referenced this pull request May 12, 2026
…debar nav + Playwright E2E + tech debt cleanup

Phase 2B scope per Manager plan 02-manager-plan.md:
- 9 page /contracts/* (list, create wizard, services, partners, partners/[id], templates, [id], [id]/acceptance, [id]/payment)
- 16 _components (15 spec + dropdown-action helper)
- Sidebar nav group "Hợp đồng" with 4 sub-items + NEW badge
- 2 Playwright spec (lifecycle E2E + pure helper component spec — 7 tests)

Phase 2A tech debt resolved:
- #1 totalAmountInWords VN num→words helper (vn-num-to-words.ts + 13 unit tests PASS)
- #2 Articles loop {#articles}{heading}{body}{/articles} + line-items loop syntax in 9 DOCX stub templates
- #4 Admin UI implemented

SDK strategy: hand-typed contracts-api.ts wrapper (594 lines) port pattern from course-map-api.ts because local Mongo+Redis not running blocks pnpm generate:api — flagged as PAUSE-CODE-PHASE2-D for post-QC Phase 3 swap. Endpoint paths + payload shapes verified byte-by-byte against contracts.controller.ts.

Tests: 60 backend PASS (+13 vs Phase 2A), 7 admin component pure-helper tests.

Pre-push CI 5/5 PASS:
1. backend build → dist/main.js 3848 bytes
2. admin pnpm build → 43 pages + 9 contracts routes
3. backend tsc: only pre-existing vi upload-spec errors (scripts/ excluded)
4. admin tsc: only pre-existing kiosk spec syntax errors
5. pnpm-lock.yaml unchanged

Status: 🟠 READY_FOR_QC

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
minhnb9897 added a commit that referenced this pull request May 25, 2026
Manager Wave 1 review BLOCKING TD resolution. 2 TDs fixed before
Wave 2B backend services start.

## TD-F062-MOM-BOUNDARY-ROLLOVER 🟡 MED 🔴 BLOCKING — RESOLVED
period-resolver.ts:
- NEW shiftMonthClamped(date, months) exported helper (lines 84-127)
  Clamps day to last-day-of-target-month to avoid JS setUTCMonth rollover
  bug (May 31 setUTCMonth(-1) → May 1 instead of April 30).
- Updated resolveCompare('mom') branch to use shiftMonthClamped (lines 225-238)
- 8 new standalone helper tests + 5 new mom boundary regression tests

Boundary verification matrix (7/7 PASS via Node REPL + 13 Jest tests):
- May 31 → April 30 (Manager bug case fixed)
- Jan 31 → Dec 31 (cross-year, no clamp)
- Mar 29 → Feb 29 LEAP YEAR 2024 (no clamp)
- Mar 29 → Feb 28 NON-LEAP 2025 (clamp to 28)
- Mar 31 → Feb 29 LEAP YEAR 2024 (clamp from 31 to 29)
- Time preservation HH/MM/SS/MS (May 22 23:59:59.123 → April 22 23:59:59.123)
- 0 month no-op

## TD-F062-VALIDATION-COMPAREKIND 🟢 LOW — RESOLVED
repeat-athlete-rate.dto.ts:
- @isin array extend từ 4 → 6 values: +'wow' +'mom' (parity với
  Wave 1 CompareKind extension)
- ApiProperty enum array + description updated
- F-026 backward compat preserved (4 original values still accepted)

Discovery: QC TD claim "6 F-026 endpoints" actually only 1 endpoint
(repeat-athlete-rate) has compareWith field. Other 5 (merchant-churn,
time-to-fill, claim-rate, geographic-demographic, refund-cancel-rate)
don't accept compareWith. TD-F062-F026-SILENT-CAPABILITY-EXPANSION
refined understanding documented IMPLEMENTATION_NOTES Section 1
Deviation #6.

## Test results
- 104/104 analytics tests PASS (Wave 1: 77 + Wave 2A: 13 new + F-058: 14)
- ZERO break F-026 backward compat
- TypeScript clean cho 3 Wave 2A files

## Files changed (3 files / 160 LoC)
- M backend/src/modules/analytics/services/period-resolver.ts (+55)
- M backend/src/modules/analytics/__tests__/period-resolver.f062.spec.ts (+104)
- M backend/src/modules/analytics/dto/repeat-athlete-rate.dto.ts (+9)

## Workflow artifacts
- 03-coder-implementation.md Wave 2A section appended (+154 LoC)
- IMPLEMENTATION_NOTES.md Wave 2A section appended (+99 LoC, 4 sub-sections:
  Deviations #5+#6 + Forced #4 + Tradeoffs 5 + Reviewer Notes)

## Wave 2B next session scope
- 5 NEW backend services (runner-analytics + race-performance +
  merchant-comparison + ga4 + export)
- 16 NEW DTOs + 12 NEW endpoints
- flushEventOverrideCache() extend +13 patterns
- PAUSE pnpm install @google-analytics/data — Danny confirm
- Verify MySQL races.type column existence PAUSE-SA-07

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
minhnb9897 added a commit that referenced this pull request May 25, 2026
QC report flagged 2 BLOCKING + 2 MED PRD-spec drifts on commit d5e31b5.
v2 patch resolves all 4:

🔴 BLOCKING #1: Endpoint URL — @get('revenue/comparison') → @get('comparison')
  per BR-SA-04 line 200 (NOT under /revenue/ namespace). Wave 3 frontend
  Tab 1 Comparison Row sẽ call /analytics/comparison?compareWith=mom per
  PRD journey table line 749 — now mounted at right path.

🔴 BLOCKING #2: Cache key drift (4 sub-issues fixed via helper extend)
  - period-resolver.ts: EXTEND buildMetricCacheKey signature (tenant scope +
    optional `extra` axis between scope và periodKey per BR-SA-04 spec)
  - analytics.service.ts: NEW resolveQueryScope + buildPeriodKey helpers;
    3 inline cache key strings replaced với buildMetricCacheKey composition
  - Result keys now PRD-compliant:
      analytics:metric:weekly-revenue:tenant:42:range:2026-01-01~2026-05-25
      analytics:metric:comparison:platform:mom:range:...
  - BR-SA-18 invalidation hook ready: Wave 2C flushEventOverrideCache()
    sẽ match analytics:metric:{weekly,monthly}-revenue:* + comparison:*

🟡 MED #3: Default 12 weeks / 12 months when no period params
  - NEW applyDefaultPeriod(query, granularity) helper
  - Returns NEW query object (no mutation) với from = today - 84/365 days
  - Called first line trong getWeeklyRevenue + getMonthlyRevenue

🟡 MED #4: buildMetricCacheKey tenant scope extension
  Fixed as part of BLOCKING #2 (single helper extension serves both)

Tests: 169/169 PASS (161 baseline + 8 NEW invariant tests):
  - revenue-endpoints.f062.spec.ts: cache key helper usage assertions,
    endpoint URL @get('comparison') guard, default period invariants,
    extractMethodBody helper generalized to non-async methods
  - period-resolver.f062.spec.ts: NEW 3 tests cho tenant scope + extra
    axis + backward compat

0 regression. tsc clean. Backward compat preserved (existing 3-arg + race
scope buildMetricCacheKey calls unaffected).

IMPLEMENTATION_NOTES.md Section 1 Deviation #10 — honest root-cause analysis
why initial Self-Review Bước 2 missed these (pattern-matched Response shape
only, không grep PRD bullet keywords Endpoint/Default/Cache). Lesson codified
cho Wave 5 memory update.

Status: READY_FOR_QC v2 (re-submit). Pending QC re-verify 4 fix items.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
minhnb9897 added a commit that referenced this pull request May 25, 2026
All 4 REJECT findings from QC report Wave 2B-1 (commit d5e31b5) verified
RESOLVED by fix patch commit a36d3b6:
  - BLOCKING #1 Endpoint URL /analytics/comparison ✓
  - BLOCKING #2 Cache key drift (4 sub-issues) ✓
  - MED #3 Default 12 weeks/12 months ✓
  - MED #4 buildMetricCacheKey tenant scope extend ✓

PRD Compliance Score: 19/19 ✅ (was 13/19 ✅ in v1).
169/169 tests PASS (was 161 + 8 NEW anti-regression invariants).
0 regression. Backward compat preserved.

Verdict: APPROVED — ready for Manager spot-check (independent code review).
Reviewer Notes top-5 revised priority in IMPLEMENTATION_NOTES Section 4.

Defense-in-depth value documented: v1 had 161 tests PASS but 4 PRD spec
drifts; QC Phase 5 line-by-line walk caught them; v2 + 8 NEW invariants
prevent re-introduction. Lesson codified for Wave 5 memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant