Skip to content

[Fraud Protection] Add overview and logs tabs to the portal#5791

Open
jimmycwc wants to merge 10 commits into
authgear:mainfrom
jimmycwc:ui-fraud-protection
Open

[Fraud Protection] Add overview and logs tabs to the portal#5791
jimmycwc wants to merge 10 commits into
authgear:mainfrom
jimmycwc:ui-fraud-protection

Conversation

@jimmycwc

@jimmycwc jimmycwc commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
2026-06-26 14 19 45 2026-06-26 14 19 27

Summary

Adds the Fraud Protection Overview and Logs experiences to the portal, backed by real Admin API data.

  • Admin API: extend the fraud protection overview with time buckets and per-IP geo data, and supporting read-store queries.
  • Overview tab: requests-over-time chart plus top source IPs / IP locations / SMS origins lists and metric cards.
  • Logs tab: filterable, paginated log table wired to the fraudProtectionLogs query, with customizable columns (persisted per app), result/reason-code/date-range filters, and a log detail screen.
  • Date range dialog: time picker support and a custom range label.
  • Fixes: format the logs timestamp column via formatDatetime (instead of the raw ISO string), and restore default-visible optional columns (reason codes, IP country) when stored preferences are empty.

Test plan

  • Overview tab renders chart and top lists for a range with data, and handles empty ranges
  • Logs tab filters (result, reason codes, date range, search) and pagination work
  • Column customization persists across reloads; defaults show reason codes + IP country
  • Timestamp column and log detail show localized, timezone-aware datetimes
  • make -C portal typecheck passes

Made with Cursor

@tung2744

tung2744 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Is this ready for review?

@tung2744

tung2744 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@jimmycwc I have added a skill for reviewing PRs in your branch. Please pull the latest commits and use it with /review-pr. Suggest to use claude sonnet 4.6 or better models.

And here are the issues discovered:

Code quality issues

  • gofmt violation: pkg/lib/audit/fraud_protection_overview.go:19-26 — struct tag alignment is off. Verified: gofmt -l pkg/lib/audit/fraud_protection_overview.go reports the file; gofmt -d shows misaligned columns in FraudProtectionOverviewSendSMS.
  • i18n regression 1: portal/src/components/fraud-protection/OverviewRequestsChart.tsx:118,125,132,189,229 — legend/tooltip labels "Blocked", "Flagged", "Total requests" and the chart title "Requests by action" are hardcoded English literals, with zero renderToString/FormattedMessage calls in the file. This is inconsistent with the sibling component added in the same commit, OverviewTopSourceIPs.tsx, which correctly routes every label through renderToString(...) with locale keys.
  • i18n regression 2: portal/src/components/fraud-protection/OverviewTopSourceIPs.tsx:32new Intl.DisplayNames(["en"], { type: "region" }) is hardcoded to "en" at module scope, so country names (e.g. "Japan (JP)") always render in English regardless of the active portal locale. The component already receives locale via useContext(Context) two lines below and other new code in this same PR (formatDatetime(locale, ...)) correctly threads locale through — this call just doesn't.
  • Dead code cleanup: the old row-expansion machinery (LogRowDetails, expandedRowId, onToggleRow) was fully removed in favor of navigating to a detail screen — clean, no orphaned references found.

Bugs

  1. "Last 24 hours" doesn't show the last 24 hoursportal/src/components/fraud-protection/FraudProtectionOverviewTab.tsx:40-58. For the "24h" option, rangeFrom/rangeTo are computed as local calendar-day boundaries (setHours(0,0,0,0) / setHours(23,59,59,999)), not a rolling 24-hour window ending now. Simulated with now = 2026-07-07T09:00 HKT: query range becomes 2026-07-06T16:00Z2026-07-07T15:59:59Z, i.e. it starts 8 hours later than a true last-24h window (2026-07-06T01:00Z) and extends ~7 hours into the future. The label (locale-data/en.json:1916, "Last 24 hours") no longer describes the behavior, and the chart will show empty future hourly buckets for most of the day.

  2. Custom column visibility silently resets when a user hides all optional columnsportal/src/components/fraud-protection/FraudProtectionLogsTab.tsx:312-315 (introduced in commit e3323fe15c). loadVisibleOptionalColumns treats an empty persisted array the same as "never set" and falls back to defaults. The ColumnsDropdown UI has no minimum-selection guard, so a user can deliberately uncheck all 4 optional columns and click Save. Verified round-trip in isolation: saving [] then reloading returns ['reasonCodes', 'ipCountry'] instead of the user's explicit empty choice — this is the exact "can't distinguish never-set from explicitly-cleared" pattern.

  3. Custom date range with time picker loses its max-date boundportal/src/graphql/portal/DateRangeDialog.tsx:56-107. The showTimePicker branch renders DateTimePicker with minDateTime={null} hardcoded and never forwards fromDatePickerMaxDate/toDatePickerMaxDate at all. FraudProtectionLogsTab.tsx:960-961 passes fromDatePickerMaxDate={lastUpdatedAt} / toDatePickerMaxDate={lastUpdatedAt} expecting the same bound the non-time-picker AuditLogScreen.tsx:695-697 relies on, but it's silently dropped. Verified DateTimePickerProps.minDateTime: "now" | null (portal/src/DateTimePicker.tsx:20) structurally has no arbitrary-max concept at all — this isn't a missed prop wiring, the replacement component can't express the old constraint. Net effect: users can now pick a future date/time in the Logs custom range dialog that was previously blocked.

Performance issues

  • Missing bound on timeBucketsQuery: pkg/lib/audit/fraud_protection_overview.go:239-248 has no LIMIT. The portal UI always sends a bounded range (today or last 7 days), but
    the GraphQL argument itself allows an unbounded/omitted range, and AuditLogFeatureConfig.RetrievalDays defaults to -1 (unlimited) — so a caller hitting the Admin API directly
    with no rangeFrom could force a scan back to the audit log's ~90-day retention with no cap on the number of returned buckets.

@tung2744 tung2744 self-assigned this Jul 7, 2026
topIPsQuery := s.SQLBuilder.
Select(
"ip_address",
`COALESCE(MODE() WITHIN GROUP (ORDER BY NULLIF(UPPER(data#>>'{payload,record,geo_location_code}'), '')), '') AS geo_country_code`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am concerned about the performance impact of this column.
Actually, we can simply resolve the geo location using the geo ip database again so we do not need to query it from the db. (Using pkg/util/geoip/geoip.go)

jimmycwc and others added 10 commits July 10, 2026 14:08
Add hourly timeBuckets to the fraud protection overview query for the
requests-by-action chart, and resolve the dominant geo country per source
IP via a two-query merge instead of a correlated subquery.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add an optional time picker to DateRangeDialog/DateTimePicker, let
DateRangeFilterDropdown render a custom range label, and add a
formatCustomDateRangeLabel helper for compact date range display.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add the requests-by-action stacked bar chart (hourly for 24h, daily for
7d), wire the overview metrics, SMS destinations, IP locations and top
source IPs to real query data, and register the Chart.js Legend plugin.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add a customizable columns dropdown with persisted visibility, make the
filter bar responsive, fetch logs and log details from the Admin API, and
remove the experimental Logs B tab variant.

Co-authored-by: Cursor <cursoragent@cursor.com>
Format the logs timestamp column with formatDatetime instead of showing the raw ISO string, and restore the default-visible optional columns (reason codes, IP country) when stored preferences are empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the expensive SQL MODE() aggregation with geoip.IPString
lookups for the top-10 source IPs, and gofmt the overview structs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add maxDateTime support to DateTimePicker and forward the existing
from/to max date props when showTimePicker is enabled so Logs custom
ranges cannot select future times.

Co-authored-by: Cursor <cursoragent@cursor.com>
Localize chart labels, use the portal locale for country names, rename
the calendar-day range to Today, honor an explicit empty column
selection in localStorage, and align the time-range control font.

Co-authored-by: Cursor <cursoragent@cursor.com>
Limit hourly time-bucket results so unbounded Admin API callers cannot
return an arbitrarily large set of buckets.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jimmycwc jimmycwc force-pushed the ui-fraud-protection branch from 5a72cd5 to 5657e46 Compare July 10, 2026 10:52
@jimmycwc

Copy link
Copy Markdown
Contributor Author

@tung2744 Thanks for the review. I've addressed the findings and rebased onto the latest main. Summary:

Fixed

  • Top source IP geo: removed the SQL MODE() aggregation; resolve country via geoip.IPString after Top-10 IP aggregation.
  • DateRangeDialog max date: added maxDateTime to DateTimePicker and wired it through the time-picker path so custom ranges can't pick beyond lastUpdatedAt.
  • i18n: chart labels/title go through renderToString; Intl.DisplayNames now uses the portal locale.
  • "Last 24 hours" label: kept calendar-day "today" behavior (as intended for the overview), renamed the option to Today.
  • Column prefs: empty [] in localStorage is now treated as an explicit choice, not "unset".
  • gofmt: cleaned up.
  • timeBuckets LIMIT: capped hourly buckets at 24 * 90 so unbounded Admin API callers can't return an arbitrarily large set.

Please take another look when you have a chance. Thanks!

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.

2 participants