feat(space): extend manager member search to username/email/phone#200
Conversation
Admin member list (GET /v1/manager/spaces/:space_id/members) previously matched keyword only against user.name and space_member.uid. Extend the keyword filter to also cover user.username, user.email and user.phone so SSO / email-login users (whose name may be empty) can be located. - db_manager: share one OR-LIKE clause between list and count queries to avoid filter drift between page rows and total; reuse existing wildcard escaping (_/%/\ treated as literals) - tests: add TestManager_Members_KeywordSearch (name/username/email/phone/ uid + list-count parity + wildcard escaping); rebuild user fixture table via DROP+CREATE so reused test DBs pick up the new username/phone columns - swagger: document the members list endpoint (first path for space module)
lml2468
left a comment
There was a problem hiding this comment.
[APPROVE]
Clean, well-scoped feature β single shared memberSearchWhere keeps list and count queries in sync, proper wildcard escaping, good test coverage.
β Highlights
memberSearchWhereshared by both list and count eliminates the "filter drift β pagination mismatch" class of bugs. Smart design.- Wildcard escaping via
escapeLike+ESCAPE '\\'β actually more correct than the user module'squeryUserListWithPageAndKeywordwhich does raw"%" + keyword + "%"without escaping. Nice. - Test coverage is thorough: each search column tested individually, list-vs-count parity asserted, underscore-as-literal pinned.
DROP + CREATEin TestMain is pragmatic β the MySQL 8ADD COLUMN IF NOT EXISTSgap is real, and the comment explains the rationale clearly.
π΅ Suggestions (non-blocking)
- No index on search columns (
u.name,u.username,u.email,u.phone): the 5-columnOR LIKE '%β¦%'guarantees a full scan on theusertable. Acceptable for an admin-only endpoint with low query volume, but worth keeping in mind if the user table grows past ~100k rows. A composite fulltext index or a dedicated search column could help if this ever becomes a bottleneck β but that's a cross-module concern, not specific to this PR.
CI: Build β | Lint β | Vet β | i18n β | Test pending.
Jerry-Xin
left a comment
There was a problem hiding this comment.
This PR is in scope for octo-server and the implementation looks mergeable; I found no blocking correctness, security, or permission issues.
π¬ Non-blocking
π‘ Warning: modules/space/swagger/api.yaml:67 references token security but the new Space Swagger document still has no securityDefinitions.token. Other module Swagger files define this explicitly, and standalone validation/tooling may treat this as an undefined security scheme. Add the standard apiKey header definition for token.
π΅ Suggestion: modules/space/api_test.go:61 recreates the test user.phone column as VARCHAR(20), while production migrations now end at VARCHAR(100). Matching production here would avoid future fixture surprises.
β Highlights
The shared memberSearchWhere helper keeps list/count filtering aligned in modules/space/db_manager.go:194 and modules/space/db_manager.go:214.
The new test covers name, username, email, phone, uid, list/count parity, and LIKE wildcard escaping.
I attempted go test ./modules/space/, but local MySQL was unavailable: dial tcp 127.0.0.1:3306: connect: connection refused.
yujiawei
left a comment
There was a problem hiding this comment.
Code Review β PR #200 (octo-server)
Verdict: APPROVED β no blocking issues. This is a focused, well-tested change that extends the admin member-search keyword filter from (name, uid) to (name, username, email, phone, uid). The implementation is sound; the notes below are non-blocking suggestions.
What I verified β
- No SQL injection.
keywordis fully parameterized end-to-end.memberSearchWhere(db_manager.go:39-48) builds the clause from a hardcoded column list and binds%keyword%positionally viabuilder.Where(clause, args...). Placeholder count (5) matches arg count (5); theESCAPEconstant contains no?. No string interpolation of user input. - No cross-space leakage.
queryMembersAdmin/countMembersAdminpinWhere("sm.space_id=?", spaceId)as a separateWhere()call before adding the OR-block.gocraft/dbrwraps eachWherecondition in its own parentheses (buildCondincondition.go), so the SQL renders asWHERE (sm.space_id=?) AND (name LIKE ? OR ... OR sm.uid LIKE ?). The space scope is correctly AND-ed; there is no AND/OR precedence bug. - LIKE wildcards escaped.
escapeLikeescapes\ % _(backslash first, correct order) and pairs with an explicitESCAPE '\\'clause, guarding againstsql_mode=NO_BACKSLASH_ESCAPES. Thewildcard escapedtest (api_manager_test.go:315-319) is genuinely meaningful: it would fail if escaping regressed. - No new PII in the response.
managerMemberRespreturns onlyuid/name/role/status/created_at/updated_at.email/phone/usernameare now searchable but are never serialized into the response body β only matched. - List/count parity. Both queries share the identical JOIN, base WHERE, and
memberSearchWhereclause under the same guard β no filter drift.user.uidis UNIQUE in both prod and the fixture, so the LEFT JOIN cannot fan outCOUNT(*)above the list row count. Orphan members (nouserrow) are handled consistently (NULL LIKEnever matches;sm.uid LIKEstill does). - Schema alignment. Production
user(modules/user/sql/20191106000003+ later ALTERs) hasusername,email,phone. The test fixture'sDROP + CREATEofuseris safe: it runs once inTestMain(schema only), per-test isolation usesDELETE FROM(not DROP), there is not.Parallel()in the package, and no FK referencesuser. go vet ./modules/space/passes.
Security notes for manual attention (non-blocking)
This PR is correctly flagged security-sensitive. Two observations that a human owner may want to acknowledge β both are pre-existing and not introduced by this diff, but this PR changes their blast radius:
-
Cross-space PII existence-oracle (P2, pre-existing model). The
/v1/managergroup is gated byrequireAdminβCheckLoginRole, which passes for any globaladmin/superAdminrole with no per-space ownership check. By widening the keyword filter toemail/phone, any platform admin can now probeGET /v1/manager/spaces/<any-space>/members?keyword=<email-or-phone>to confirm whether a given email/phone belongs to a member of an arbitrary space. The authorization model is unchanged by this PR, but the PII surface it exposes is wider. Suggest confirming this is acceptable for the admin tier and that these searches are covered by audit logging. -
No keyword min-length/format validation (nit). A 1-character keyword becomes
LIKE '%x%'across PII columns, enabling broad partial sweeps. Low impact (an admin can already list all members of a space, and PII is not returned), but a minimum keyword length and/or audit logging would reduce the probe surface.
Minor / maintainability (non-blocking)
- Pagination has no unique tiebreaker (P2, pre-existing).
queryMembersAdminorders bysm.role DESC, sm.created_at ASCwith LIMIT/OFFSET but no unique final sort key.created_atis second-granularity and members bulk-added in the same second share it; with equal role, MySQL's order for tied rows is undefined and can drift between pages, skipping/duplicating a boundary row. Untouched by this PR, but it sits in the changed function β consider appending.OrderAsc("sm.uid"). - Test fixture diverges from prod schema (nit). The fixture
usertable omits prod columns (short_no,status,role, etc.) and declares search columns nullable where prod isNOT NULL. Harmless for this PR (only the 5 search columns are touched), but a latent trap for future code paths that depend on the omitted columns. - Implicit test invariant (nit). The
list and count share filtersubtest (expects 2 forexample.com) silently relies onu-ownerhaving nouserrow. An inline comment notingu-owneris intentionally left without auserrow would prevent a future fixture change from breaking it.
Overall: clean, correct, and backward-compatible. Approving.
Summary
The admin member list endpoint
GET /v1/manager/spaces/:space_id/memberspreviously matched thekeywordquery param only againstuser.nameandspace_member.uid. This extends the keyword filter to also coveruser.username,user.emailanduser.phone.Motivation: SSO / email-login users often have an empty
name, so admins could not locate them by display name. Allowing username/email/phone makes the member search usable for those accounts (consistent with the user-module list search behavior).Changes
OR LIKEclause shared by both the list and count queries, so the page rows and the total count always use the same filter (no drift). Reuses the existing wildcard escaping (_/%/\treated as literals viaESCAPE).TestManager_Members_KeywordSearchcovering match-by name / username / email / phone / uid, list-vs-count parity, and wildcard escaping. Theuserfixture table is now rebuilt withDROP + CREATE(instead ofCREATE TABLE IF NOT EXISTS) so a reused test database picks up the newusername/phonecolumns β MySQL 8 has noADD COLUMN IF NOT EXISTS.Behavior / compatibility
keywordmatches against is widened. Fully backward compatible for existing clients.page_indexdefault 1,page_sizedefault 20 / max 200) and admin auth are unchanged.Test plan
go vet ./modules/space/go test ./modules/space/(full package green)TestManager_Members_KeywordSearchβ verified each of name/username/email/phone/uid matches exactly the expected member; list and count share the filter; underscore is escaped (not a wildcard)userschema" case and confirmedTestMainnow self-heals viaDROP + CREATETracking: #201