fix(moderation): write the address ranges the ModCP lookup reads - #121
Merged
Conversation
The ModCP address lookup and the admin member search's IP filter both query users.registration_ip_prefix and users.last_ip_prefix, and nothing ever wrote either column: every lookup answered "Ranges on record: none" while still writing a modcp.ip_lookup audit row. Registration now stores the truncated range it came from, and a successful sign-in rewrites the last-visit range. The range is resolved where every other request-derived address already is — remoteAddress() and truncateIp() in the app — and passed into the domain as an argument on the existing RequestContext, so no package reaches for request state. Only the /24 or /48 prefix crosses that boundary; a full address is never handed to a repository. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014kP43A5shJWmAKqkBovhaF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes MEI-52.
The bug
The ModCP "Address lookup" screen and the admin member search's IP filter both query
users.registration_ip_prefixandusers.last_ip_prefix— and nothing in the board ever wrote either column./modcp/ipreads both (packages/db/src/modcp-repo.ts,ipPrefixesFor/ipMatches); the member search filters on the same pair (packages/db/src/user-admin-repo.ts), as does the "shares a network" panel on a member's admin page.packages/db/src/account-repos.ts), sign-in and the presence write all omitted them. Repo-wide, only the schema and the readers mentioned the columns; the db tests passed because they populated them with rawUPDATEs.So every lookup answered "Ranges on record: none" forever, while still writing a
modcp.ip_lookupaudit row for a query against empty columns — and the page's lede ("Finds accounts that share a stored address range with a member") anddocs/mybb-parity.md("it matches the truncated prefix the board stores") both described something that could not happen.The fix
Make the columns real, using the plumbing that already exists.
NewAccount.registrationIpPrefix.AccountRepository.recordLastIpPrefix(userId, prefix).truncateIp(await remoteAddress())inauth-actions.ts— the same pairrecordAdminActionuses for the moderator log. No second truncation was written.RequestContextthatregister()andlogin()already accept, alongsideip. No package importsnext/headers, and nothing inpackages/reaches for request state.Decisions
Sign-in, not the presence write, for
last_ip_prefix. The presence path (sessions.touchLocation) fires once a minute per active member and would put ausersUPDATE on the read path of every page view; a sign-in is the moment the board actually learns an account is being used from somewhere, and it costs one UPDATE per session. The cost is that a member signed in from before this ships shows no last-visit range until they sign in again, and a member who only ever resumes by remember-me cookie keeps the range of their last real sign-in. Both are written down indocs/mybb-parity.md.Only the truncated prefix crosses the boundary. The app truncates before the value enters the domain, so a full address is never handed to a repository — the same posture the moderator log already holds.
truncateIpyields/24for v4 and/48for v6; nothing else was touched, and the "IP starts with" filter's own copy ("Only a prefix is ever stored, so this is a network") is now true.posts.ip_prefixis deliberately left unwritten — it is not silently dead by accident. Nothing reads it: the address lookup searches the twouserscolumns only. Writing it would mean threading a prefix throughNewThreadRecord,NewReplyRecord, both insert sites inthread-writes.ts, the copy path inthread-tools.tsand the fixture repositories, to produce a second address trail with no reader. Out of scope here, and said so indocs/mybb-parity.mdrather than left to be rediscovered.The MyBB importer still carries neither range. It does not read
regip/lastipfrom the source board, so an imported board's lookups stay empty until its members register or sign in here. Recorded as a cost in the parity doc; changing the import contract is a separate change.No
next buildrisk: nothing underapps/community/appand no"use client"component was touched; no domain package is value-imported anywhere new.Validation
pnpm verify— green (exit 0). 310 test files, 5728 tests. The two warnings it prints (consistent-type-importsinadmin.test.ts,no-orphansonapps/web/src/format.ts) are pre-existing onmainand unrelated.DATA_SOURCE=fixture pnpm build— green (exit 0).pnpm test:e2ewas not run.New tests, each proved to fail first by breaking the fix and restoring it:
packages/accounts/src/service.test.tsIdentityService→ 3 red/24and leaves an unrelated one out; an account with no range matches nothingpackages/db/src/account-repos.test.ts(PGlite, boot once + clear inbeforeEach)apps/community/src/server/auth-actions.test.tsremoteAddress()withouttruncateIp→ 2 redThe db test drives the writer (
PostgresAccountRepository) and the reader (PostgresModCpRepository) in one place, which is the pair that was silently disagreeing.Docs updated in the same commit:
docs/mybb-parity.md(when each range is written, why sign-in and not the presence write, and the costs) anddocs/operating.md(the address now keys five things, not four, andTRUSTED_PROXY_HOPS=0records no ranges either).🤖 Generated with Claude Code
Generated by Claude Code