fix(bunyip-api): handler correctness for webhooks, admin, auth, dedup #122

Closed
David wants to merge 1 commit from david/fix/bunyip-api-handler-correctness into main AGit
Owner

Stripe webhook: a checkout.session.completed without our user_id metadata now returns Ok(()) and skips instead of 4xx-ing, ending the infinite Stripe retry storm; the lifetime price lock now reads the price id from the session line items rather than storing the subscription id (sub_...) in locked_price_id.

Admin endpoints: PUT users/{id}/status with active=true now reactivates (new UserRepository::restore clears deleted_at) and 404s only when no soft-deleted row exists, instead of always 422-ing; POST /admin/test-email switches from AuthenticatedUser to AdminUser so a non-admin can no longer trigger their own welcome email; get_system_health now reuses a shared collect_dashboard_stats helper (no duplicated count SQL) and propagates DB errors instead of swallowing them via .ok().flatten(); SystemHealth.uptime_seconds is populated from a process-start Instant registered in app state.

Auth redirect: auth_redirect now validates the target against config.web_origin (like logout_redirect) so redirects stop 422-ing when COOKIE_DOMAIN is unset, and its per-request cookie/target diagnostics drop from info! to debug!.

Dedup: the byte-for-byte check_rate_limit copies in auth.rs and totp.rs and the inline variant in feedback.rs collapse to one shared handlers::check_rate_limit. The JwtService extraction unifies on the documented canonical path req.app_data::<Arc>() (jwt is registered as a bare Arc, so the web::Data<Arc> form in admin.rs would 500); admin_reset_password and impersonate_user move to that path.

Misc: the dev-only no-payment POST /v1/memberships/subscribe route and handler are removed; the unused PaginationQuery.page field and the leftover ListFeedbackQuery.page_size alias are dropped; export_feedback now surfaces request_id on the response instead of discarding it; delete_account calls revoke_op_sessions so OIDC OP/SSO sessions die with the account (as logout_all does); oci_auth::too_many receives the RateLimitConfig that actually tripped so Retry-After is correct for throughput and per-IP denials instead of 0.

The application_field arbitrary-field finding does not apply: application updates already flow through the typed UpdateApplication struct, so only defined columns are settable.

#BUNYIP-77

Stripe webhook: a checkout.session.completed without our user_id metadata now returns Ok(()) and skips instead of 4xx-ing, ending the infinite Stripe retry storm; the lifetime price lock now reads the price id from the session line items rather than storing the subscription id (sub_...) in locked_price_id. Admin endpoints: PUT users/{id}/status with active=true now reactivates (new UserRepository::restore clears deleted_at) and 404s only when no soft-deleted row exists, instead of always 422-ing; POST /admin/test-email switches from AuthenticatedUser to AdminUser so a non-admin can no longer trigger their own welcome email; get_system_health now reuses a shared collect_dashboard_stats helper (no duplicated count SQL) and propagates DB errors instead of swallowing them via .ok().flatten(); SystemHealth.uptime_seconds is populated from a process-start Instant registered in app state. Auth redirect: auth_redirect now validates the target against config.web_origin (like logout_redirect) so redirects stop 422-ing when COOKIE_DOMAIN is unset, and its per-request cookie/target diagnostics drop from info! to debug!. Dedup: the byte-for-byte check_rate_limit copies in auth.rs and totp.rs and the inline variant in feedback.rs collapse to one shared handlers::check_rate_limit. The JwtService extraction unifies on the documented canonical path req.app_data::<Arc<JwtService>>() (jwt is registered as a bare Arc, so the web::Data<Arc<JwtService>> form in admin.rs would 500); admin_reset_password and impersonate_user move to that path. Misc: the dev-only no-payment POST /v1/memberships/subscribe route and handler are removed; the unused PaginationQuery.page field and the leftover ListFeedbackQuery.page_size alias are dropped; export_feedback now surfaces request_id on the response instead of discarding it; delete_account calls revoke_op_sessions so OIDC OP/SSO sessions die with the account (as logout_all does); oci_auth::too_many receives the RateLimitConfig that actually tripped so Retry-After is correct for throughput and per-IP denials instead of 0. The application_field arbitrary-field finding does not apply: application updates already flow through the typed UpdateApplication struct, so only defined columns are settable. #BUNYIP-77
fix(bunyip-api): handler correctness for webhooks, admin, auth, dedup
All checks were successful
Check / fmt / clippy / build / test (pull_request) Successful in 1m4s
Create release / Create release from merged PR (pull_request) Has been skipped
0cfe37b04d
Stripe webhook: a checkout.session.completed without our user_id metadata now returns Ok(()) and skips instead of 4xx-ing, ending the infinite Stripe retry storm; the lifetime price lock now reads the price id from the session line items rather than storing the subscription id (sub_...) in locked_price_id.

Admin endpoints: PUT users/{id}/status with active=true now reactivates (new UserRepository::restore clears deleted_at) and 404s only when no soft-deleted row exists, instead of always 422-ing; POST /admin/test-email switches from AuthenticatedUser to AdminUser so a non-admin can no longer trigger their own welcome email; get_system_health now reuses a shared collect_dashboard_stats helper (no duplicated count SQL) and propagates DB errors instead of swallowing them via .ok().flatten(); SystemHealth.uptime_seconds is populated from a process-start Instant registered in app state.

Auth redirect: auth_redirect now validates the target against config.web_origin (like logout_redirect) so redirects stop 422-ing when COOKIE_DOMAIN is unset, and its per-request cookie/target diagnostics drop from info! to debug!.

Dedup: the byte-for-byte check_rate_limit copies in auth.rs and totp.rs and the inline variant in feedback.rs collapse to one shared handlers::check_rate_limit. The JwtService extraction unifies on the documented canonical path req.app_data::<Arc<JwtService>>() (jwt is registered as a bare Arc, so the web::Data<Arc<JwtService>> form in admin.rs would 500); admin_reset_password and impersonate_user move to that path.

Misc: the dev-only no-payment POST /v1/memberships/subscribe route and handler are removed; the unused PaginationQuery.page field and the leftover ListFeedbackQuery.page_size alias are dropped; export_feedback now surfaces request_id on the response instead of discarding it; delete_account calls revoke_op_sessions so OIDC OP/SSO sessions die with the account (as logout_all does); oci_auth::too_many receives the RateLimitConfig that actually tripped so Retry-After is correct for throughput and per-IP denials instead of 0.

The application_field arbitrary-field finding does not apply: application updates already flow through the typed UpdateApplication struct, so only defined columns are settable.

#BUNYIP-77
David closed this pull request 2026-06-12 11:59:46 +02:00
All checks were successful
Check / fmt / clippy / build / test (pull_request) Successful in 1m4s
Create release / Create release from merged PR (pull_request) Has been skipped

Pull request closed

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!122
No description provided.