fix(web): edge validation hardening + tier Stripe IDs (BUNYIP-112/113/115/122) #162

Merged
YousifShkara merged 1 commit from fix/web-validation-bundle into main 2026-06-17 04:12:53 +02:00
Owner

Bundle of four medium-severity validation gaps in bunyip-web. The domain layer enforces correctness in every case; the gap was that the web edge posted unbounded / unformatted input to the API, so malformed values surfaced as raw 500s when they hit DB VARCHAR caps and numeric inputs silently coerced on parse failure.

New shared validator module bunyip-web/src/handlers/validate.rs (8 helpers + 9 unit tests): trim_bounded, trim_bounded_opt, slug, url_opt, parse_i32, parse_i32_opt, email, email_opt. Used by every fixed form below.

BUNYIP-113: application group sort_order was parse::<i64>().unwrap_or(0), so non-numeric input silently became 0 and i64 values > i32::MAX truncated against the INTEGER column. group_body now returns Result<Value, String> and parse_i32 rejects non-numeric / out-of-range with an inline error. Both application_group_create and application_group_save short-circuit the API call when validation fails, rendering the form again via the existing error_box pattern.

BUNYIP-112: create_app_body had only trim on identity fields. Junk slug like " $$$ " was accepted even though slug is load-bearing for OCI repo paths in Application::oci_pull_image; a > 255-char name hit the DB cap as a raw 500. Now name / display_name / container_name are length-capped (200 each, mirroring the column widths), slug uses the strict ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ shape, distribution coordinates are bounded at 200 chars each, and forgejo_package (when sent on a generic_package source) is bounded. New unit test pins " $$$ " and a 300-char name as rejected.

BUNYIP-115: public feedback form posted name / subject / email unbounded. Over-length input round-tripped to the API and surfaced as a raw 500 from the DB VARCHAR caps; malformed email was accepted silently. feedback_post now bounds name (100), subject (200), email (254 + shape check), message (16000) before calling the API. Inline error renders via the existing error_box path. The form view gains matching maxlength= attributes so the browser bounds input before submission.

BUNYIP-122: tier settings model already carried Stripe *_price_id and *_product_id fields and the API's UpdateTierConfigRequest already accepted them, but the bunyip-web tier form rendered only the four slot/trial inputs. Added six new inputs (free_price / lifetime_product / early_adopter_price / early_adopter_product / standard_price / standard_product) plus the matching three fields to web's TierConfigResponse shape. TierForm carries them as optional strings; the JSON body omits empty values so the API's tri-state-by-omission semantics apply (a blank field leaves the persisted value untouched; explicit clear is left for v2 once the API grows the shape).

New dependency: url = "2" in bunyip-web/Cargo.toml (already in tree transitively via reqwest, declared so the validator can ::url::Url::parse for icon_url / source_code_url / health_check_url / webhook_url shape checks).

Tests: 9 new validator tests + 1 new create-app body test covering the BUNYIP-112 acceptance cases. cargo test -p bunyip-web all green. The pre-existing bunyip-domain::config::tests::test_config_defaults failure is on main, not introduced by this PR.

#BUNYIP-112
#BUNYIP-113
#BUNYIP-115
#BUNYIP-122

Bundle of four medium-severity validation gaps in bunyip-web. The domain layer enforces correctness in every case; the gap was that the web edge posted unbounded / unformatted input to the API, so malformed values surfaced as raw 500s when they hit DB VARCHAR caps and numeric inputs silently coerced on parse failure. New shared validator module `bunyip-web/src/handlers/validate.rs` (8 helpers + 9 unit tests): `trim_bounded`, `trim_bounded_opt`, `slug`, `url_opt`, `parse_i32`, `parse_i32_opt`, `email`, `email_opt`. Used by every fixed form below. BUNYIP-113: application group `sort_order` was `parse::<i64>().unwrap_or(0)`, so non-numeric input silently became 0 and i64 values > i32::MAX truncated against the INTEGER column. `group_body` now returns Result<Value, String> and `parse_i32` rejects non-numeric / out-of-range with an inline error. Both `application_group_create` and `application_group_save` short-circuit the API call when validation fails, rendering the form again via the existing `error_box` pattern. BUNYIP-112: `create_app_body` had only trim on identity fields. Junk slug like `" $$$ "` was accepted even though slug is load-bearing for OCI repo paths in `Application::oci_pull_image`; a > 255-char name hit the DB cap as a raw 500. Now name / display_name / container_name are length-capped (200 each, mirroring the column widths), slug uses the strict `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` shape, distribution coordinates are bounded at 200 chars each, and `forgejo_package` (when sent on a generic_package source) is bounded. New unit test pins `" $$$ "` and a 300-char name as rejected. BUNYIP-115: public feedback form posted `name` / `subject` / `email` unbounded. Over-length input round-tripped to the API and surfaced as a raw 500 from the DB VARCHAR caps; malformed email was accepted silently. `feedback_post` now bounds name (100), subject (200), email (254 + shape check), message (16000) before calling the API. Inline error renders via the existing `error_box` path. The form view gains matching `maxlength=` attributes so the browser bounds input before submission. BUNYIP-122: tier settings model already carried Stripe `*_price_id` and `*_product_id` fields and the API's `UpdateTierConfigRequest` already accepted them, but the bunyip-web tier form rendered only the four slot/trial inputs. Added six new inputs (free_price / lifetime_product / early_adopter_price / early_adopter_product / standard_price / standard_product) plus the matching three fields to web's `TierConfigResponse` shape. `TierForm` carries them as optional strings; the JSON body omits empty values so the API's tri-state-by-omission semantics apply (a blank field leaves the persisted value untouched; explicit clear is left for v2 once the API grows the shape). New dependency: `url = "2"` in `bunyip-web/Cargo.toml` (already in tree transitively via reqwest, declared so the validator can `::url::Url::parse` for icon_url / source_code_url / health_check_url / webhook_url shape checks). Tests: 9 new validator tests + 1 new create-app body test covering the BUNYIP-112 acceptance cases. `cargo test -p bunyip-web` all green. The pre-existing `bunyip-domain::config::tests::test_config_defaults` failure is on main, not introduced by this PR. #BUNYIP-112 #BUNYIP-113 #BUNYIP-115 #BUNYIP-122
fix(web): edge validation hardening + tier Stripe IDs (BUNYIP-112/113/115/122)
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m21s
16b8591515
Bundle of four medium-severity validation gaps in bunyip-web. The domain layer enforces correctness in every case; the gap was that the web edge posted unbounded / unformatted input to the API, so malformed values surfaced as raw 500s when they hit DB VARCHAR caps and numeric inputs silently coerced on parse failure.

New shared validator module `bunyip-web/src/handlers/validate.rs` (8 helpers + 9 unit tests): `trim_bounded`, `trim_bounded_opt`, `slug`, `url_opt`, `parse_i32`, `parse_i32_opt`, `email`, `email_opt`. Used by every fixed form below.

BUNYIP-113: application group `sort_order` was `parse::<i64>().unwrap_or(0)`, so non-numeric input silently became 0 and i64 values > i32::MAX truncated against the INTEGER column. `group_body` now returns Result<Value, String> and `parse_i32` rejects non-numeric / out-of-range with an inline error. Both `application_group_create` and `application_group_save` short-circuit the API call when validation fails, rendering the form again via the existing `error_box` pattern.

BUNYIP-112: `create_app_body` had only trim on identity fields. Junk slug like `" $$$ "` was accepted even though slug is load-bearing for OCI repo paths in `Application::oci_pull_image`; a > 255-char name hit the DB cap as a raw 500. Now name / display_name / container_name are length-capped (200 each, mirroring the column widths), slug uses the strict `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` shape, distribution coordinates are bounded at 200 chars each, and `forgejo_package` (when sent on a generic_package source) is bounded. New unit test pins `" $$$ "` and a 300-char name as rejected.

BUNYIP-115: public feedback form posted `name` / `subject` / `email` unbounded. Over-length input round-tripped to the API and surfaced as a raw 500 from the DB VARCHAR caps; malformed email was accepted silently. `feedback_post` now bounds name (100), subject (200), email (254 + shape check), message (16000) before calling the API. Inline error renders via the existing `error_box` path. The form view gains matching `maxlength=` attributes so the browser bounds input before submission.

BUNYIP-122: tier settings model already carried Stripe `*_price_id` and `*_product_id` fields and the API's `UpdateTierConfigRequest` already accepted them, but the bunyip-web tier form rendered only the four slot/trial inputs. Added six new inputs (free_price / lifetime_product / early_adopter_price / early_adopter_product / standard_price / standard_product) plus the matching three fields to web's `TierConfigResponse` shape. `TierForm` carries them as optional strings; the JSON body omits empty values so the API's tri-state-by-omission semantics apply (a blank field leaves the persisted value untouched; explicit clear is left for v2 once the API grows the shape).

New dependency: `url = "2"` in `bunyip-web/Cargo.toml` (already in tree transitively via reqwest, declared so the validator can `::url::Url::parse` for icon_url / source_code_url / health_check_url / webhook_url shape checks).

Tests: 9 new validator tests + 1 new create-app body test covering the BUNYIP-112 acceptance cases. `cargo test -p bunyip-web` all green. The pre-existing `bunyip-domain::config::tests::test_config_defaults` failure is on main, not introduced by this PR.

#BUNYIP-112
#BUNYIP-113
#BUNYIP-115
#BUNYIP-122
YousifShkara deleted branch fix/web-validation-bundle 2026-06-17 04:12:53 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!162
No description provided.