Skip to content

[codex] Expose inactive admin users#694

Draft
PierreLeGuen wants to merge 1 commit into
mainfrom
codex/admin-user-visibility
Draft

[codex] Expose inactive admin users#694
PierreLeGuen wants to merge 1 commit into
mainfrom
codex/admin-user-visibility

Conversation

@PierreLeGuen
Copy link
Copy Markdown
Contributor

Summary

  • Include inactive users by default in /v1/admin/users, including the include_organizations=true path.
  • Add admin-only OAuth identity fields (auth_provider, provider_user_id) to admin user responses.
  • Add server-side search and is_active filters with totals based on the exact filtered result set.
  • Add e2e coverage for inactive user lookup and provider identity search.

Why

Admins could not reliably diagnose login failures caused by hidden inactive user rows or OAuth identity collisions without direct production DB access. This makes those rows discoverable from admin tooling.

Validation

  • cargo fmt
  • cargo check -p api
  • git diff --check
  • cargo test -p api --test e2e_all test_admin_list_users_finds_inactive_and_oauth_identity_fields -- --nocapture compiled, then failed during local test DB bootstrap because Postgres was not accepting connections (Connection refused).

@PierreLeGuen PierreLeGuen deployed to Cloud API test env May 30, 2026 06:22 — with GitHub Actions Active
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the admin user listing functionality by adding support for filtering users by active status and searching across multiple fields, including email, username, display name, user ID, auth provider, and provider user ID. It also exposes the auth_provider and provider_user_id fields in the admin user responses and includes comprehensive E2E tests. The review feedback highlights a logic error in the organization name filter within list_with_organizations that incorrectly includes users without organizations during searches, and points out a sorting inconsistency where users are ordered by UUID instead of creation date.

Comment on lines +375 to 377
AND ($3::TEXT IS NULL
OR o.name ILIKE ('%' || $3 || '%') ESCAPE '\'
OR o.id IS NULL)
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.

high

There is a logic error in the organization name filter. When $3 (the organization search query) is provided (i.e., not NULL), the condition OR o.id IS NULL will still evaluate to TRUE for any user who does not belong to any organization. As a result, searching for a specific organization name will incorrectly return all matching users plus all users who have no organizations at all.

To fix this, remove OR o.id IS NULL. When $3 is NULL, the entire clause is satisfied by $3::TEXT IS NULL, which correctly includes users without organizations.

Suggested change
AND ($3::TEXT IS NULL
OR o.name ILIKE ('%' || $3 || '%') ESCAPE '\'
OR o.id IS NULL)
AND ($3::TEXT IS NULL
OR o.name ILIKE ('%' || $3 || '%') ESCAPE '\\')

Comment on lines +426 to 428
AND ($5::TEXT IS NULL
OR o.name ILIKE ('%' || $5 || '%') ESCAPE '\'
OR o.id IS NULL)
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.

high

Similarly, the organization name filter here has the same logic error. When $5 is provided, OR o.id IS NULL incorrectly includes all users without organizations in the search results.

Remove OR o.id IS NULL to ensure only users belonging to the searched organization are returned.

Suggested change
AND ($5::TEXT IS NULL
OR o.name ILIKE ('%' || $5 || '%') ESCAPE '\'
OR o.id IS NULL)
AND ($5::TEXT IS NULL
OR o.name ILIKE ('%' || $5 || '%') ESCAPE '\\')

AND ($5::TEXT IS NULL
OR o.name ILIKE ('%' || $5 || '%') ESCAPE '\'
OR o.id IS NULL)
ORDER BY u.id, o.created_at ASC NULLS LAST
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.

medium

Using ORDER BY u.id as the primary sort key (required by DISTINCT ON (u.id)) results in users being returned in an arbitrary order (sorted by their UUID). This is inconsistent with list_admin, which returns users sorted by created_at DESC (newest first), making the admin UI behavior unpredictable depending on whether organizations are included.

Consider wrapping the query in a subquery or CTE to apply DISTINCT ON first, and then sort the final paginated result set by created_at DESC on the outer query.

Copy link
Copy Markdown
Contributor Author

@PierreLeGuen PierreLeGuen left a comment

Choose a reason for hiding this comment

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

Reviewed the admin user-visibility changes. Logic is sound — inactive users are now included by default, the multi-field search is correctly parameterized and LIKE-escaped (nice dedup into escape_like_query), and the org-path count/list use COUNT(DISTINCT u.id) / DISTINCT ON (u.id) so the new joins don't inflate totals. A few inline notes below; none are blocking.

Ok((result, total_count))
}

async fn get_active_user_count(&self) -> Result<i64> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

get_active_user_count is removed from the AdminRepository trait here, but the underlying inherent method UserRepository::get_active_user_count (crates/database/src/repositories/user.rs:206) now has no callers anywhere in the workspace. Worth deleting it in this PR so it doesn't linger as dead code.

params.limit,
params.offset,
params.include_organizations,
params.search,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Minor privacy note: search is logged here verbatim, and an admin searching by email / display name puts PII into the log line. It's debug! (not emitted at prod info level), so this is low-risk and consistent with the pre-existing search_by_name logging — just flagging given the repo's strict logging rules. Consider logging only search.is_some() if you want to be conservative.

let count_row = client
.query_one(
r#"
SELECT COUNT(*) as total_count
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Non-blocking perf note: when search is set this runs a leading-wildcard ILIKE ('%' || $ || '%') across 6 columns, and it's executed twice per request (count + page), neither of which can use a btree index → sequential scan of users. Fine at current admin-tooling scale; if the users table grows large this is the first thing that'll get slow. (When search is NULL the $::TEXT IS NULL guard short-circuits, so the no-search path is unaffected.)

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