Address Gadfly review on #7: garden cap, dim validation, shared errors
Build image / build-and-push (push) Successful in 5s
Build image / build-and-push (push) Successful in 5s
Fixes from the PR #26 adversarial review (graded 18 real / 0 false positive). Correctness / security - maxGardenCM fixed to 10_000 (100 m), matching its comment — it was 100_000 cm (1 km), 10x too lax (5 models flagged this). - Dimension validation now rejects NaN/Inf (which slip past naive comparisons) and subnormal-tiny positives, via a finite [1cm, 100m] check. Name (200) and notes (10_000) are length-capped so untrusted input can't balloon storage. - Update version binding is `required,min=1`, so a negative/zero version is a 400, not a 409. Maintainability / performance - One unified writeServiceError (new errors.go) maps every auth + resource sentinel; writeResourceError removed. writeVersionConflict and parseIDParam moved to errors.go (shared, not in the gardens feature file). - Request structs share an embedded gardenFields (one toInput). - CreateGarden uses INSERT ... RETURNING (one round-trip). - ListGardensForOwner has a defensive LIMIT (pagination is post-v1). Tests: name/notes length, NaN/Inf/subnormal dims, dimension-at-cap valid, negative/zero version -> 400. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -210,29 +209,3 @@ func (h *handlers) clearSessionCookie(c *gin.Context) {
|
||||
func mustActor(c *gin.Context) *domain.User {
|
||||
return c.MustGet(actorKey).(*domain.User)
|
||||
}
|
||||
|
||||
// writeServiceError maps a service-layer sentinel error to pansy's JSON error
|
||||
// envelope. Login failures never leak which of email/password was wrong.
|
||||
func writeServiceError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrInvalidCredentials):
|
||||
writeAPIError(c, http.StatusUnauthorized, "INVALID_CREDENTIALS", "invalid email or password")
|
||||
case errors.Is(err, domain.ErrEmailTaken):
|
||||
writeAPIError(c, http.StatusConflict, "EMAIL_TAKEN", "an account with that email already exists")
|
||||
case errors.Is(err, domain.ErrRegistrationClosed):
|
||||
writeAPIError(c, http.StatusForbidden, "REGISTRATION_CLOSED", "registration is closed")
|
||||
case errors.Is(err, domain.ErrLocalAuthDisabled):
|
||||
writeAPIError(c, http.StatusForbidden, "LOCAL_AUTH_DISABLED", "local authentication is disabled")
|
||||
case errors.Is(err, domain.ErrOIDCNoEmail):
|
||||
writeAPIError(c, http.StatusBadRequest, "OIDC_NO_EMAIL", "the identity provider returned no email")
|
||||
case errors.Is(err, domain.ErrOIDCEmailUnverified):
|
||||
writeAPIError(c, http.StatusForbidden, "OIDC_EMAIL_UNVERIFIED", "the identity provider's email is not verified")
|
||||
case errors.Is(err, domain.ErrOIDCIdentityConflict):
|
||||
writeAPIError(c, http.StatusConflict, "OIDC_IDENTITY_CONFLICT", "this identity conflicts with an existing account")
|
||||
case errors.Is(err, domain.ErrInvalidInput):
|
||||
writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "invalid input")
|
||||
default:
|
||||
slog.Error("api: unhandled service error", "error", err)
|
||||
writeAPIError(c, http.StatusInternalServerError, "INTERNAL", "internal error")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user