Skip to content

[server] allow anyone to list active users#11

Merged
capcom6 merged 2 commits into
masterfrom
server/allow-to-list-users
Apr 27, 2026
Merged

[server] allow anyone to list active users#11
capcom6 merged 2 commits into
masterfrom
server/allow-to-list-users

Conversation

@capcom6
Copy link
Copy Markdown
Contributor

@capcom6 capcom6 commented Apr 23, 2026

Summary by CodeRabbit

  • New Features

    • Users now have a name field; API responses include name
    • Added authenticated user search by name with pagination and query validation
  • Documentation

    • Updated API docs/Swagger for users endpoints and new search path; response schemas include name
  • Chores

    • Removed example module
    • Adjusted user routes/authorization scoping
    • Database migration to backfill and enforce non-null user names
    • Prometheus client dependency reclassified to indirect

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@capcom6 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 58 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa19a12c-864c-4717-9a73-9d8c16c9dd9d

📥 Commits

Reviewing files that changed from the base of the PR and between 1a8bfff and 901b96d.

📒 Files selected for processing (16)
  • go.mod
  • internal/db/migrations/20260424000000_add_user_name.sql
  • internal/server/auth/dto.go
  • internal/server/docs/docs.go
  • internal/server/dto/response.go
  • internal/server/tasks/dto.go
  • internal/server/tasks/dto_attachments.go
  • internal/server/tasks/dto_comments.go
  • internal/server/users/dto.go
  • internal/server/users/handler.go
  • internal/users/domain.go
  • internal/users/errors.go
  • internal/users/models.go
  • internal/users/repository.go
  • internal/users/service.go
  • requests.http
📝 Walkthrough

Walkthrough

Adds a non-nullable name column for users (migration, models, repository, service, DTOs, handlers, and docs), exposes a new authenticated GET /users/search endpoint with pagination, removes the entire internal/example module, and moves a Prometheus client dependency to indirect in go.mod.

Changes

Cohort / File(s) Summary
DB migration
internal/db/migrations/20260424000000_add_user_name.sql
Adds users.name column (nullable -> backfilled from email local-part -> set NOT NULL), creates (status, name) index, and provides down migration.
Users domain & persistence
internal/users/domain.go, internal/users/models.go, internal/users/repository.go, internal/users/service.go, internal/users/errors.go
Adds User.Name field; persists/derives Name from email local-part; adds Repository.Search (case-insensitive prefix match with escaping) and Service.Search (validates empty query -> ErrEmptyQuery).
Server DTOs & handlers
internal/server/auth/dto.go, internal/server/dto/response.go, internal/server/tasks/... (dto.go, dto_attachments.go, dto_comments.go), internal/server/users/dto.go, internal/server/users/handler.go
Replaces email with name in brief/response DTOs and mapping functions; initializes Name in task/attachment/comment DTOs; adds SearchQuery DTO and toListResponse; introduces handleSearch endpoint; adjusts /users route authorization and maps ErrEmptyQuery -> 400.
API docs & requests
internal/server/docs/docs.go, requests.http
Swagger: adds GET /users/search, adds name to user schemas, introduces dto.UserBriefList, adjusts tags/descriptions; adds /users/search request example.
Removed example module
internal/example/doc.go, internal/example/config.go, internal/example/domain.go, internal/example/errors.go, internal/example/metrics.go, internal/example/models.go, internal/example/module.go, internal/example/repository.go, internal/example/service.go
Deletes the entire internal/example package (docs, types, errors, metrics, models, repository, service, FX module).
go.mod
go.mod
Moves github.com/prometheus/client_golang v1.23.2 from direct to indirect dependency (// indirect) without version change.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Client
    participant HTTP as HTTP Handler (/users/search)
    participant Service as Users Service
    participant Repo as Users Repository
    participant DB as Database

    Client->>HTTP: GET /users/search?query=...&limit=&offset=
    HTTP->>Service: s.Search(ctx, q, pagination)
    Service->>Repo: repo.Search(ctx, q, pagination)
    Repo->>DB: SELECT ... WHERE lower(name) LIKE escaped_q ORDER BY name LIMIT/OFFSET
    DB-->>Repo: rows + total
    Repo-->>Service: []User, total
    Service-->>HTTP: []User, total
    HTTP-->>Client: 200 OK with paginated UserBriefList
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[server] allow anyone to list active users' accurately describes the main objective of the pull request, which involves adding a new /users/search endpoint accessible to authenticated users and restructuring user listing permissions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

🤖 Pull request artifacts

Platform File
🐳 Docker GitHub Container Registry
🍎 Darwin arm64 backend_Darwin_arm64.tar.gz
🍎 Darwin x86_64 backend_Darwin_x86_64.tar.gz
🐧 Linux arm64 backend_Linux_arm64.tar.gz
🐧 Linux i386 backend_Linux_i386.tar.gz
🐧 Linux x86_64 backend_Linux_x86_64.tar.gz
🪟 Windows arm64 backend_Windows_arm64.zip
🪟 Windows i386 backend_Windows_i386.zip
🪟 Windows x86_64 backend_Windows_x86_64.zip

@coderabbitai coderabbitai Bot added the codex label Apr 24, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
internal/db/migrations/20260424000000_add_user_name.sql (1)

16-16: Nit: stray --- separator between Up and Down sections.

--- is technically a valid SQL line comment but is non-standard in Goose migrations (the convention is a blank line). Consider removing it for consistency with the other migrations in this repo.

🧹 Proposed tweak
 -- +goose StatementEnd
----
+
 -- +goose Down
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/migrations/20260424000000_add_user_name.sql` at line 16, Remove
the stray '---' separator line between the Up and Down sections in this
migration: locate the literal '---' that sits between the Up block and the Down
block and delete it so the migration uses a single blank line (consistent with
other Goose migrations) while preserving the surrounding SQL statements in the
Up and Down sections.
internal/users/repository.go (1)

104-128: Consider indexing for the prefix search as the user table grows.

LOWER(name) LIKE 'prefix%' can use an index only if there is a functional/expression index on LOWER(name) (PostgreSQL) with the appropriate operator class (text_pattern_ops / varchar_pattern_ops) so the LIKE optimization kicks in. Without it, every search becomes a full scan of the active-user set. If you expect any meaningful user volume, add an index in a follow-up migration, e.g. for Postgres:

CREATE INDEX users_name_lower_prefix_idx
  ON users (LOWER(name) varchar_pattern_ops)
  WHERE status = 'active';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/users/repository.go` around lines 104 - 128, The Search method in
Repository uses "LOWER(name) LIKE '%s%%'" which will force full table scans
unless the DB has a functional prefix index; add a follow-up DB migration that
creates a conditional functional index on LOWER(name) with the appropriate
operator class for prefix searches (and filter by status = 'active') so queries
generated by Search (the LOWER(name) LIKE prefix) can use an index (e.g. create
an index on LOWER(name) using varchar_pattern_ops/text_pattern_ops with WHERE
status = 'active'). Ensure the migration targets your SQL dialect (Postgres) and
documents the rationale for the Repository.Search query.
internal/users/models.go (1)

62-75: Consider using strings.Cut for emailUsername.

The hand-rolled loop can be expressed more idiomatically and with identical fallback semantics:

♻️ Proposed refactor
-func emailUsername(email string) string {
-	// Best-effort parsing; email is expected to contain '@' due to validation elsewhere.
-	at := 0
-	for i := range len(email) {
-		if email[i] == '@' {
-			at = i
-			break
-		}
-	}
-	if at <= 0 {
-		return email
-	}
-	return email[:at]
+func emailUsername(email string) string {
+	// Best-effort parsing; email is expected to contain '@' due to validation elsewhere.
+	if local, _, ok := strings.Cut(email, "@"); ok && local != "" {
+		return local
+	}
+	return email
 }

(Requires adding "strings" to the import block.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/users/models.go` around lines 62 - 75, The emailUsername function
uses a manual byte-loop to find '@'; replace it with strings.Cut to split the
email on "@" and return the left part when cut succeeds, otherwise return the
original email to preserve fallback semantics; add "strings" to the import block
and update emailUsername to call strings.Cut(email, "@") and return the
appropriate value based on the boolean result.
internal/server/users/handler.go (1)

98-112: Nit: @Failure 403 for /users/search can never fire.

The route has no role gating (only JWT auth), so the handler has no code path that returns 403. The annotation is harmless but slightly misleading for API consumers. Consider dropping the 403 line (same applies to the generated Swagger block in docs.go).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/users/handler.go` around lines 98 - 112, The OpenAPI
annotation for the search handler includes a spurious "@Failure 403" entry that
can never occur because the route only enforces JWT auth and the handler
(handleSearch) has no role-based checks; remove the "@Failure 403" line from the
comment block above handleSearch in users/handler.go and also remove the
corresponding 403 failure entry in the generated Swagger block in docs.go so the
docs accurately reflect possible responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/db/migrations/20260424000000_add_user_name.sql`:
- Around line 3-15: The migration currently adds the users.name column but no
index; update the migration to create an index on the name column (e.g.,
idx_users_name) in the Up section and drop that index in Down so prefix searches
can use the index, and ensure the column uses the existing case-insensitive
collation (utf8mb4_unicode_ci) or explicitly set it if needed; then update the
repository query at the function in internal/users/repository.go (around the
code that calls LOWER(name) LIKE ...) to remove the LOWER(...) wrapper and use
name LIKE ? (prefix%) so the index is usable.

---

Nitpick comments:
In `@internal/db/migrations/20260424000000_add_user_name.sql`:
- Line 16: Remove the stray '---' separator line between the Up and Down
sections in this migration: locate the literal '---' that sits between the Up
block and the Down block and delete it so the migration uses a single blank line
(consistent with other Goose migrations) while preserving the surrounding SQL
statements in the Up and Down sections.

In `@internal/server/users/handler.go`:
- Around line 98-112: The OpenAPI annotation for the search handler includes a
spurious "@Failure 403" entry that can never occur because the route only
enforces JWT auth and the handler (handleSearch) has no role-based checks;
remove the "@Failure 403" line from the comment block above handleSearch in
users/handler.go and also remove the corresponding 403 failure entry in the
generated Swagger block in docs.go so the docs accurately reflect possible
responses.

In `@internal/users/models.go`:
- Around line 62-75: The emailUsername function uses a manual byte-loop to find
'@'; replace it with strings.Cut to split the email on "@" and return the left
part when cut succeeds, otherwise return the original email to preserve fallback
semantics; add "strings" to the import block and update emailUsername to call
strings.Cut(email, "@") and return the appropriate value based on the boolean
result.

In `@internal/users/repository.go`:
- Around line 104-128: The Search method in Repository uses "LOWER(name) LIKE
'%s%%'" which will force full table scans unless the DB has a functional prefix
index; add a follow-up DB migration that creates a conditional functional index
on LOWER(name) with the appropriate operator class for prefix searches (and
filter by status = 'active') so queries generated by Search (the LOWER(name)
LIKE prefix) can use an index (e.g. create an index on LOWER(name) using
varchar_pattern_ops/text_pattern_ops with WHERE status = 'active'). Ensure the
migration targets your SQL dialect (Postgres) and documents the rationale for
the Repository.Search query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1588b7f2-37f5-40c1-bf47-f5053dd3a496

📥 Commits

Reviewing files that changed from the base of the PR and between ad836d0 and 0f082e4.

📒 Files selected for processing (24)
  • go.mod
  • internal/db/migrations/20260424000000_add_user_name.sql
  • internal/example/config.go
  • internal/example/doc.go
  • internal/example/domain.go
  • internal/example/errors.go
  • internal/example/metrics.go
  • internal/example/models.go
  • internal/example/module.go
  • internal/example/repository.go
  • internal/example/service.go
  • internal/server/auth/dto.go
  • internal/server/docs/docs.go
  • internal/server/dto/response.go
  • internal/server/tasks/dto.go
  • internal/server/tasks/dto_attachments.go
  • internal/server/tasks/dto_comments.go
  • internal/server/users/dto.go
  • internal/server/users/handler.go
  • internal/users/domain.go
  • internal/users/models.go
  • internal/users/repository.go
  • internal/users/service.go
  • requests.http
💤 Files with no reviewable changes (9)
  • internal/example/domain.go
  • internal/example/config.go
  • internal/example/service.go
  • internal/example/errors.go
  • internal/example/metrics.go
  • internal/example/doc.go
  • internal/example/repository.go
  • internal/example/module.go
  • internal/example/models.go

Comment thread internal/db/migrations/20260424000000_add_user_name.sql
@capcom6 capcom6 force-pushed the server/allow-to-list-users branch from 054c145 to 8d721f5 Compare April 24, 2026 07:59
@capcom6 capcom6 marked this pull request as ready for review April 24, 2026 23:17
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/server/docs/docs.go (1)

1120-1182: ⚠️ Potential issue | 🟡 Minor

Generated spec is out of sync with the handler annotations.

This block was generated from a version of handleSearch whose annotations differ from what's currently in internal/server/users/handler.go (parameter name name here vs. query there; 403 response here vs. only 401 there). Regenerate docs.go with swag init after the handler annotations are finalized to avoid shipping a misleading API spec.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/docs/docs.go` around lines 1120 - 1182, The generated OpenAPI
block in docs.go is out of sync with the current handler annotations for
handleSearch: update the handler annotations in handleSearch to the desired
final state (confirm whether the query parameter is named "query" or "name" and
whether 403 should be present), then regenerate the spec by running swag init to
rebuild docs.go so the parameter name and responses (401/403) match the handler;
ensure you re-run swag init after finalizing annotations for the handleSearch
function to avoid mismatches.
🧹 Nitpick comments (1)
internal/users/repository.go (1)

105-113: Consider rejecting empty name to avoid an unbounded match.

If name == "", pattern becomes "%" which matches every active user, effectively turning Search into an unrestricted listing of active users. The handler-side DTO marks query as required, but a defensive guard here keeps the repository contract safe regardless of how it is called:

 func (r *Repository) Search(
 	ctx context.Context,
 	name string,
 	pagination *Pagination,
 ) ([]User, int, error) {
+	if name == "" {
+		return []User{}, 0, nil
+	}
 	// Escape LIKE metacharacters in user input so they are matched literally.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/users/repository.go` around lines 105 - 113, Repository.Search
currently builds a LIKE pattern from name which becomes "%" for an empty string
and matches all users; add a defensive guard at the top of Search (in function
Repository.Search) that checks if strings.TrimSpace(name) == "" and returns an
appropriate error (e.g., ErrInvalidArgument or a context-appropriate error)
instead of proceeding to build pattern and query, ensuring callers receive a
clear failure rather than an unbounded match.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/server/users/handler.go`:
- Around line 88-101: The Swagger comment for the handleSearch handler has a
parameter name mismatch and an incorrect 403 failure entry; update the `@Param`
line to use the DTO's query name ("name") to match the SearchQuery binding
(query:"name") and remove any `@Failure` documentation for 403 Forbidden, then
re-run swag to regenerate docs.go so the generated spec matches the SearchQuery
DTO and the actual route behavior.

In `@internal/users/repository.go`:
- Around line 115-118: The WHERE clause is malformed: replace the incorrect
Where("LIKE ?", pattern) in the Search query built with r.db.NewSelect() (the
query variable that chains Model(&models), StatusActive and pattern) with a
proper column reference — e.g. Where("LOWER(name) LIKE ?", pattern) (or
Where("LOWER(name) LIKE ? ESCAPE '\\\\'", pattern) if you want explicit ESCAPE)
so the SQL becomes WHERE status = ? AND LOWER(name) LIKE ? and the query in the
Search method runs correctly.

---

Duplicate comments:
In `@internal/server/docs/docs.go`:
- Around line 1120-1182: The generated OpenAPI block in docs.go is out of sync
with the current handler annotations for handleSearch: update the handler
annotations in handleSearch to the desired final state (confirm whether the
query parameter is named "query" or "name" and whether 403 should be present),
then regenerate the spec by running swag init to rebuild docs.go so the
parameter name and responses (401/403) match the handler; ensure you re-run swag
init after finalizing annotations for the handleSearch function to avoid
mismatches.

---

Nitpick comments:
In `@internal/users/repository.go`:
- Around line 105-113: Repository.Search currently builds a LIKE pattern from
name which becomes "%" for an empty string and matches all users; add a
defensive guard at the top of Search (in function Repository.Search) that checks
if strings.TrimSpace(name) == "" and returns an appropriate error (e.g.,
ErrInvalidArgument or a context-appropriate error) instead of proceeding to
build pattern and query, ensuring callers receive a clear failure rather than an
unbounded match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c7fa1d7b-8cf9-40e3-9f5a-fb3e407e5595

📥 Commits

Reviewing files that changed from the base of the PR and between 0f082e4 and 8d721f5.

📒 Files selected for processing (15)
  • go.mod
  • internal/db/migrations/20260424000000_add_user_name.sql
  • internal/server/auth/dto.go
  • internal/server/docs/docs.go
  • internal/server/dto/response.go
  • internal/server/tasks/dto.go
  • internal/server/tasks/dto_attachments.go
  • internal/server/tasks/dto_comments.go
  • internal/server/users/dto.go
  • internal/server/users/handler.go
  • internal/users/domain.go
  • internal/users/models.go
  • internal/users/repository.go
  • internal/users/service.go
  • requests.http
✅ Files skipped from review due to trivial changes (7)
  • requests.http
  • internal/users/domain.go
  • internal/server/dto/response.go
  • internal/server/tasks/dto_comments.go
  • go.mod
  • internal/server/tasks/dto_attachments.go
  • internal/server/tasks/dto.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/server/auth/dto.go
  • internal/users/service.go
  • internal/users/models.go
  • internal/db/migrations/20260424000000_add_user_name.sql
  • internal/server/users/dto.go

Comment thread internal/server/users/handler.go
Comment thread internal/users/repository.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/users/service.go (1)

61-67: Consider trimming the query before the empty check.

A whitespace‑only q (e.g. " ") bypasses this guard and reaches the repository, where it becomes a LIKE/prefix pattern that matches nothing — not a correctness bug, but it sends a pointless query and an empty page back to the caller. Trimming once here keeps the contract consistent with ErrEmptyQuery and saves a DB round‑trip.

♻️ Proposed refactor
 func (s *Service) Search(ctx context.Context, q string, pagination *Pagination) ([]User, int, error) {
-	if q == "" {
-		return nil, 0, ErrEmptyQuery
-	}
-
-	return s.repo.Search(ctx, q, pagination)
+	q = strings.TrimSpace(q)
+	if q == "" {
+		return nil, 0, ErrEmptyQuery
+	}
+
+	return s.repo.Search(ctx, q, pagination)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/users/service.go` around lines 61 - 67, Service.Search currently
checks q == "" but doesn't trim whitespace, so inputs like "   " bypass the
empty check and cause a pointless DB query; update Search to trim the query (use
strings.TrimSpace) before the emptiness check and return ErrEmptyQuery for
trimmed-empty input, then pass the trimmed value to s.repo.Search; ensure the
strings package is imported and only trim once at the start of Service.Search.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/server/users/handler.go`:
- Around line 88-118: handleSearch currently returns toListResponse (which
reuses GetResponse exposing email/status/created_at/updated_at) allowing any
authenticated caller to see PII; fix by introducing a public search DTO (e.g.,
SearchResponse and SearchResponseItem with fields ID, Name, Role) and a
converter function toSearchResponse(items []users.User, total int)
SearchResponse that omits email and other sensitive fields, then update
handleSearch to return c.JSON(toSearchResponse(usersList, total)) and adjust the
Swagger `@Success` annotation to reference the new SearchResponse type instead of
ListResponse/GetResponse.

---

Nitpick comments:
In `@internal/users/service.go`:
- Around line 61-67: Service.Search currently checks q == "" but doesn't trim
whitespace, so inputs like "   " bypass the empty check and cause a pointless DB
query; update Search to trim the query (use strings.TrimSpace) before the
emptiness check and return ErrEmptyQuery for trimmed-empty input, then pass the
trimmed value to s.repo.Search; ensure the strings package is imported and only
trim once at the start of Service.Search.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e4c8c84-6412-4bf7-83fc-8d98fdeecfb6

📥 Commits

Reviewing files that changed from the base of the PR and between 8d721f5 and 89a590a.

📒 Files selected for processing (6)
  • internal/server/docs/docs.go
  • internal/server/users/dto.go
  • internal/server/users/handler.go
  • internal/users/errors.go
  • internal/users/repository.go
  • internal/users/service.go
✅ Files skipped from review due to trivial changes (1)
  • internal/users/errors.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/users/repository.go

Comment thread internal/server/users/handler.go
@capcom6 capcom6 force-pushed the server/allow-to-list-users branch from 89a590a to 1a8bfff Compare April 27, 2026 02:12
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/server/tasks/dto.go (1)

106-138: ⚠️ Potential issue | 🟡 Minor

Tracking pre-existing issue: stubbed UserBrief author/assignee.

Outside this PR's scope, but worth flagging: the comment on line 107 promises that newTaskResponse "requires fetching author and assignee from the users service," yet Name, Role, and CreatedAt are still hard-coded to "" (just like the previous Email: ""). With name now actually meaningful (populated for /users/search), the inconsistency is more visible — task responses give back essentially {id: N, name: "", role: "", created_at: ""} for author/assignee, which front-ends will likely render as blank.

Consider opening a follow-up to populate these via the users service (batch lookup by IDs) so consumers of /tasks see the same data shape as /users/search.

Want me to open a follow-up issue tracking the author/assignee enrichment in newTaskResponse?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/tasks/dto.go` around lines 106 - 138, newTaskResponse
currently returns stubbed dto.UserBrief for Author and Assignee
(Name/Role/CreatedAt empty); fix by enriching these fields from the users
service: perform a batch lookup for task.AuthorID and task.AssigneeID (use or
add a users service method like UsersService.LookupByIDs or a users client that
returns map[userID]User), then populate dto.UserBrief.Name, Role, CreatedAt from
the fetched User data (or change newTaskResponse signature to accept a
pre-fetched map of users to avoid blocking calls inside the function); ensure
date fields are converted to RFC3339 for CreatedAt and handle missing users by
leaving the existing ID-only stub.
internal/server/docs/docs.go (1)

1043-1119: ⚠️ Potential issue | 🟠 Major

Remove the Users tag or restore admin-only enforcement to match the implementation.

The route handler still enforces jwtauth.WithRole(users.RoleAdmin) on GET /users (internal/server/users/handler.go:43), but the Swagger documentation tags it as Users and describes it as "List all users"—implying general authenticated access. Either revert the docs to the Admin tag, or if the intent is to open this endpoint to all authenticated users, remove the role guard from the handler. Note: GET /users/search is the unguarded endpoint for listing active users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/docs/docs.go` around lines 1043 - 1119, The OpenAPI docs for
the GET /users endpoint are out of sync with the handler protection: the handler
still uses jwtauth.WithRole(users.RoleAdmin) while docs tag/summary imply
general access; either (A) update the docs entry in docs.go for the "/users" GET
to use an "Admin" tag and a summary/description that clearly marks it as
admin-only (e.g., "Admin: List all users") so it matches
jwtauth.WithRole(users.RoleAdmin), or (B) remove the role guard
jwtauth.WithRole(users.RoleAdmin) from the GET /users route handler so the
endpoint becomes general-authenticated like the docs (ensure other auth
middleware remains intact); pick one approach and make the corresponding change
to the symbol docs.go "/users" GET entry or the route registration that applies
jwtauth.WithRole(users.RoleAdmin).
🧹 Nitpick comments (3)
internal/server/users/dto.go (1)

53-66: Optional: simplify defaultSearchQuery / SearchQuery.

Setting Query: "" in defaultSearchQuery() is redundant — it's the zero value, and the required,min=1 validator will reject an empty/missing value anyway. You could drop the explicit Query: "" for clarity. Also consider embedding a shared dto.PaginationQuery (as TaskListQuery does) to keep Limit/Offset definitions DRY across DTOs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/users/dto.go` around lines 53 - 66, Remove the redundant
explicit Query: "" assignment in defaultSearchQuery and simplify SearchQuery by
embedding the shared pagination DTO; replace Limit and Offset fields in
SearchQuery with an embedded dto.PaginationQuery (like TaskListQuery does) and
update defaultSearchQuery to return SearchQuery{} (or construct with the
embedded pagination defaults) so Limit/Offset defaults come from the shared
dto.PaginationQuery while leaving Query as the required string field validated
by the tags.
internal/server/dto/response.go (1)

20-39: LGTM, with one nit.

Mapping logic is clean. Minor: ToUserBrief takes *users.User while ToUserBriefList's lo.Map callback receives users.User by value and then takes &u. That copies each element before re-addressing it. If the underlying users.User is large or hot-path, switching the callback to func(u *users.User, _ int) (when supported by the slice type — here you'd iterate []users.User with index) avoids the copy. Not critical given typical page sizes ≤ 100.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/dto/response.go` around lines 20 - 39, The ToUserBriefList
currently maps []users.User and uses lo.Map with a value receiver which causes a
copy when taking &u; update ToUserBriefList to avoid copying by iterating by
index and passing the address of each element to ToUserBrief (e.g., loop for i
:= range items and call ToUserBrief(&items[i])), or alternatively change the
function signature to accept []*users.User and map pointers directly; ensure
references to ToUserBrief and lo.Map/users.User are updated accordingly.
internal/server/docs/docs.go (1)

1177-1194: Double-tagging PATCH /users/{id} with both Admin and Users.

Adding Users alongside Admin causes this admin-only mutation to also appear under the public-looking Users tag in Swagger UI. If the access control is unchanged (admin-only per the description), keeping just Admin is clearer; otherwise the dual tag suggests it's accessible to regular users. Recommend dropping Users here unless authz was intentionally relaxed.

Suggested fix (in source annotations, then regenerate)

In the handler's swag annotations for PATCH /users/{id}:

- // `@Tags` Admin Users
+ // `@Tags` Admin

Then regenerate internal/server/docs/docs.go with swag init.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/docs/docs.go` around lines 1177 - 1194, The PATCH /users/{id}
operation is incorrectly tagged with both "Admin" and "Users", which exposes an
admin-only mutation under the public "Users" tag; update the handler's swag
annotations for the PATCH /users/{id} endpoint to remove the "Users" tag so only
"Admin" remains, then run swag init to regenerate internal/server/docs/docs.go
(look for the PATCH /users/{id} annotations in the corresponding handler
function to apply the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/server/docs/docs.go`:
- Around line 1043-1119: The OpenAPI docs for the GET /users endpoint are out of
sync with the handler protection: the handler still uses
jwtauth.WithRole(users.RoleAdmin) while docs tag/summary imply general access;
either (A) update the docs entry in docs.go for the "/users" GET to use an
"Admin" tag and a summary/description that clearly marks it as admin-only (e.g.,
"Admin: List all users") so it matches jwtauth.WithRole(users.RoleAdmin), or (B)
remove the role guard jwtauth.WithRole(users.RoleAdmin) from the GET /users
route handler so the endpoint becomes general-authenticated like the docs
(ensure other auth middleware remains intact); pick one approach and make the
corresponding change to the symbol docs.go "/users" GET entry or the route
registration that applies jwtauth.WithRole(users.RoleAdmin).

In `@internal/server/tasks/dto.go`:
- Around line 106-138: newTaskResponse currently returns stubbed dto.UserBrief
for Author and Assignee (Name/Role/CreatedAt empty); fix by enriching these
fields from the users service: perform a batch lookup for task.AuthorID and
task.AssigneeID (use or add a users service method like UsersService.LookupByIDs
or a users client that returns map[userID]User), then populate
dto.UserBrief.Name, Role, CreatedAt from the fetched User data (or change
newTaskResponse signature to accept a pre-fetched map of users to avoid blocking
calls inside the function); ensure date fields are converted to RFC3339 for
CreatedAt and handle missing users by leaving the existing ID-only stub.

---

Nitpick comments:
In `@internal/server/docs/docs.go`:
- Around line 1177-1194: The PATCH /users/{id} operation is incorrectly tagged
with both "Admin" and "Users", which exposes an admin-only mutation under the
public "Users" tag; update the handler's swag annotations for the PATCH
/users/{id} endpoint to remove the "Users" tag so only "Admin" remains, then run
swag init to regenerate internal/server/docs/docs.go (look for the PATCH
/users/{id} annotations in the corresponding handler function to apply the
change).

In `@internal/server/dto/response.go`:
- Around line 20-39: The ToUserBriefList currently maps []users.User and uses
lo.Map with a value receiver which causes a copy when taking &u; update
ToUserBriefList to avoid copying by iterating by index and passing the address
of each element to ToUserBrief (e.g., loop for i := range items and call
ToUserBrief(&items[i])), or alternatively change the function signature to
accept []*users.User and map pointers directly; ensure references to ToUserBrief
and lo.Map/users.User are updated accordingly.

In `@internal/server/users/dto.go`:
- Around line 53-66: Remove the redundant explicit Query: "" assignment in
defaultSearchQuery and simplify SearchQuery by embedding the shared pagination
DTO; replace Limit and Offset fields in SearchQuery with an embedded
dto.PaginationQuery (like TaskListQuery does) and update defaultSearchQuery to
return SearchQuery{} (or construct with the embedded pagination defaults) so
Limit/Offset defaults come from the shared dto.PaginationQuery while leaving
Query as the required string field validated by the tags.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76ababdb-6ea7-4eeb-9d44-07625143a8ba

📥 Commits

Reviewing files that changed from the base of the PR and between 89a590a and 1a8bfff.

📒 Files selected for processing (25)
  • go.mod
  • internal/db/migrations/20260424000000_add_user_name.sql
  • internal/example/config.go
  • internal/example/doc.go
  • internal/example/domain.go
  • internal/example/errors.go
  • internal/example/metrics.go
  • internal/example/models.go
  • internal/example/module.go
  • internal/example/repository.go
  • internal/example/service.go
  • internal/server/auth/dto.go
  • internal/server/docs/docs.go
  • internal/server/dto/response.go
  • internal/server/tasks/dto.go
  • internal/server/tasks/dto_attachments.go
  • internal/server/tasks/dto_comments.go
  • internal/server/users/dto.go
  • internal/server/users/handler.go
  • internal/users/domain.go
  • internal/users/errors.go
  • internal/users/models.go
  • internal/users/repository.go
  • internal/users/service.go
  • requests.http
💤 Files with no reviewable changes (9)
  • internal/example/models.go
  • internal/example/module.go
  • internal/example/errors.go
  • internal/example/repository.go
  • internal/example/metrics.go
  • internal/example/config.go
  • internal/example/domain.go
  • internal/example/service.go
  • internal/example/doc.go
✅ Files skipped from review due to trivial changes (5)
  • requests.http
  • internal/users/errors.go
  • internal/users/domain.go
  • go.mod
  • internal/db/migrations/20260424000000_add_user_name.sql
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/server/tasks/dto_comments.go
  • internal/server/auth/dto.go
  • internal/server/tasks/dto_attachments.go
  • internal/users/service.go
  • internal/users/repository.go
  • internal/users/models.go
  • internal/server/users/handler.go

Co-authored-by: Copilot <copilot@github.com>
@capcom6 capcom6 force-pushed the server/allow-to-list-users branch from 0075764 to 901b96d Compare April 27, 2026 03:03
@capcom6 capcom6 merged commit e20d666 into master Apr 27, 2026
7 checks passed
@capcom6 capcom6 deleted the server/allow-to-list-users branch April 27, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant