Skip to content

fix(dashboard-api): team name validation in dashboard API#2347

Open
ben-fornefeld wants to merge 1 commit intomainfrom
fix/dashboard-api-team-name-update-validation
Open

fix(dashboard-api): team name validation in dashboard API#2347
ben-fornefeld wants to merge 1 commit intomainfrom
fix/dashboard-api-team-name-update-validation

Conversation

@ben-fornefeld
Copy link
Copy Markdown
Member

Summary

  • enforce the same team name validation in dashboard-api as the dashboard uses for team name updates
  • trim names server-side before persisting so backend behavior matches the dashboard zod schema
  • tighten the dashboard OpenAPI spec for this route and regenerate the embedded spec

Details

The dashboard validates team names with these rules:

  • trim surrounding whitespace
  • require 1-32 characters after trimming
  • allow only alphanumeric chunks separated by spaces, underscores, hyphens, or dots

OpenAPI can enforce structural constraints like minLength, maxLength, pattern, and additionalProperties: false, and this service already runs the oapi request validator middleware before handlers. But OpenAPI does not normalize the input, so the handler still needs validation to preserve parity with the dashboard's .trim() behavior before writing to the database.

Testing

  • go test ./packages/dashboard-api/internal/handlers/...

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 9, 2026

PR Summary

Medium Risk
Tightens server-side validation for PATCH /teams/{teamID} (trim/length/pattern), which may start rejecting previously accepted names and could impact existing clients relying on looser constraints.

Overview
Enforces stricter server-side team name validation on PATCH /teams/{teamID} by trimming input before persisting and rejecting empty/over-32-character names or names with disallowed characters, aligning backend behavior with the dashboard. The OpenAPI schema for UpdateTeamRequest is tightened (additionalProperties: false, maxLength: 32, and a pattern), the generated embedded swagger spec is regenerated, and new handler tests cover the name validation edge cases.

Reviewed by Cursor Bugbot for commit 307762d. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 307762d9bd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +468 to +469
maxLength: 32
pattern: '^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Permit trim-before-validate names in UpdateTeamRequest schema

PatchTeamsTeamID now trims name in validateUpdateTeamName, but the new OpenAPI constraints reject padded inputs before the handler runs. With OapiRequestValidatorWithOptions enabled in packages/dashboard-api/main.go, requests like {"name":" Acme Inc "} or names that are <=32 after trimming but >32 raw are returned as 400 during schema validation, so the new server-side normalization path is never reached. This regresses the intended trim-parity behavior for non-dashboard clients.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 307762d. Configure here.

minLength: 1
maxLength: 255
maxLength: 32
pattern: '^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OpenAPI pattern prevents server-side whitespace trimming

Medium Severity

The OpenAPI spec pattern on the name field (^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$) does not permit leading or trailing whitespace. Since OapiRequestValidatorWithOptions middleware validates the raw request body against this schema before the handler runs, any name with surrounding whitespace (e.g. " Acme Inc ") is rejected at the middleware level. The handler's strings.TrimSpace is therefore unreachable in production, contradicting the PR's stated goal of preserving parity with the dashboard's .trim() behavior. The unit test passes only because it calls validateUpdateTeamName directly, bypassing the middleware.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 307762d. Configure here.

}
})
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New test uses manual assertions instead of testify

Low Severity

The new TestValidateUpdateTeamName function uses manual t.Fatalf comparisons (e.g. if err == nil { t.Fatalf(...) }, if got != tt.expected { t.Fatalf(...) }) instead of require.NoError, require.Error, require.EqualError, and assert.Equal from github.com/stretchr/testify. This violates the team convention for unified test failure messages.

Fix in Cursor Fix in Web

Triggered by learned rule: Use testify require/assert instead of manual t.Errorf in Go tests

Reviewed by Cursor Bugbot for commit 307762d. Configure here.

minLength: 1
maxLength: 255
maxLength: 32
pattern: '^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The pattern added here rejects strings with leading/trailing whitespace at the oapi request-validator middleware layer, before the handler's strings.TrimSpace can normalize them. A client sending name with surrounding spaces gets a 400 from the middleware rather than having it trimmed and stored, which contradicts the stated goal of server-side trimming.

The TestValidateUpdateTeamName trimmed-valid-name test calls validateUpdateTeamName directly, bypassing the middleware, so it does not catch this.

If accepting untrimmed inputs is intentional, remove or widen the pattern in the spec. If the middleware should always reject them, the strings.TrimSpace in validateUpdateTeamName is dead code for the HTTP endpoint.

@ben-fornefeld ben-fornefeld changed the title Enforce dashboard team name validation in dashboard API fix(dashboard-api): team name validation in dashboard API Apr 9, 2026
Comment on lines +95 to +108
func validateUpdateTeamName(name string) (string, error) {
trimmedName := strings.TrimSpace(name)
if trimmedName == "" {
return "", errors.New("Team name cannot be empty")
}

if len(trimmedName) > 32 {
return "", errors.New("Team name cannot be longer than 32 characters")
}

if !teamNamePattern.MatchString(trimmedName) {
return "", errors.New("Names can only contain letters and numbers, separated by spaces, underscores, hyphens, or dots")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 The strings.TrimSpace in validateUpdateTeamName is dead code: the OpenAPI pattern ^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$ anchors at both ends with [a-zA-Z0-9], so the OapiRequestValidatorWithOptions middleware rejects any whitespace-padded name with a 400 before the handler is ever invoked. The PR's stated goal of "trim names server-side before persisting" is therefore not achieved through the HTTP interface, and the "trimmed valid name" test case (input " Acme Inc ") exercises a code path unreachable via normal HTTP traffic. Either relax the OpenAPI pattern to ^\s*[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*\s*$ to allow normalization, or remove the TrimSpace and its test case to match the stricter rejection behavior.

Extended reasoning...

What the bug is and how it manifests

The PR introduces strings.TrimSpace inside validateUpdateTeamName (team_update.go:96) with the stated intent of normalizing whitespace-padded names before persisting them, mirroring the dashboard's .trim() behavior. However, the same PR also adds an OpenAPI pattern constraint of '^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$' to the name field in the spec (spec/openapi-dashboard.yml). Because the pattern anchors at both ends using ^ and $ and requires the string to begin and end with [a-zA-Z0-9], any name with leading or trailing whitespace fails the pattern match outright.

The specific code path that triggers it

main.go registers OapiRequestValidatorWithOptions middleware (line 221) before api.RegisterHandlers (line 244). This means every incoming request passes through the OpenAPI validator first. When a client sends {"name": " Acme Inc "}, the validator evaluates the name value against the spec's pattern. The value " Acme Inc " starts with a space character, which does not match [a-zA-Z0-9], so the middleware immediately returns a 400 and the PatchTeamsTeamID handler is never called. The strings.TrimSpace on line 96 therefore never executes for any whitespace-padded input delivered via HTTP.

Why existing code doesn't prevent it

The validateUpdateTeamName function is unit-tested in isolation (TestValidateUpdateTeamName) and does correctly trim and accept " Acme Inc ". This passes because the test calls the function directly, bypassing the middleware entirely. In integration (real HTTP traffic), the guard is the middleware, not the handler, so the trimming logic is never reached for the inputs it was designed to handle.

What the impact would be

The PR's documented goal—"trim names server-side before persisting so backend behavior matches the dashboard zod schema"—is not met. A client that sends a whitespace-padded name (e.g. from a form that does not pre-trim) receives a 400 rejection instead of having the name silently normalized. This is a stricter policy than the dashboard's own behavior, creating an inconsistency rather than parity. Additionally, the "trimmed valid name" test case gives a false sense of confidence that the trimming feature works end-to-end.

How to fix it

Two consistent options exist:

  1. Allow and normalize: Relax the OpenAPI pattern to permit optional surrounding whitespace, e.g. '^\s*[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*\s*$'. The handler's TrimSpace then becomes reachable and serves its purpose.
  2. Reject strictly: Remove strings.TrimSpace from validateUpdateTeamName and delete the "trimmed valid name" test case. Document that whitespace-padded names are rejected at the spec validation layer (update the PR description accordingly).

Step-by-step proof

  1. Client sends: PATCH /teams/{teamID} with body {"name": " Acme Inc "}
  2. OapiRequestValidatorWithOptions middleware decodes the body and validates name against the embedded swagger spec.
  3. Spec pattern ^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$ is checked against " Acme Inc ".
  4. First character is ' ' (space); [a-zA-Z0-9] does not match space → pattern check fails.
  5. Middleware returns HTTP 400; PatchTeamsTeamID handler is never called.
  6. validateUpdateTeamName and its strings.TrimSpace are never executed.
  7. The trimmed value "Acme Inc" is never persisted; the PR's normalization goal is not achieved.

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