server: minor audit polish (5 fixes from Ultra Fuzz Run #1)
- validate.rs: render "Must be at least 2 characters" for 1-char
slugs across project/collection/blog endpoints; empty stays blank
- promo_codes.rs:291: document why the final param_idx increment is
elided (unused_assignments) so the next added field knows to restore
- main.rs: explicit .with_http_only(true) on SessionManagerLayer
- versions.rs: LEAST(SUM, i64::MAX)::BIGINT clamp before cast to guard
against pathological row counts overflowing the i64
- billing.rs: tip-refund lookup now logs an explicit error line via
inspect_err before propagating
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 files changed,
+48 insertions,
-8 deletions
| 288 |
288 |
|
}
|
| 289 |
289 |
|
if max_uses.is_some() {
|
| 290 |
290 |
|
sets.push(format!("max_uses = ${param_idx}"));
|
| 291 |
|
- |
// param_idx += 1;
|
|
291 |
+ |
// Final SET clause; param_idx is never read after this point, so the
|
|
292 |
+ |
// increment is elided to avoid an unused_assignments warning. Restore
|
|
293 |
+ |
// it if a new optional field is added below.
|
| 292 |
294 |
|
}
|
| 293 |
295 |
|
|
| 294 |
296 |
|
if sets.is_empty() {
|
| 317 |
317 |
|
/// Sum all version file sizes for a given item (for storage decrement on item delete).
|
| 318 |
318 |
|
#[tracing::instrument(skip_all)]
|
| 319 |
319 |
|
pub async fn sum_file_sizes_for_item(pool: &PgPool, item_id: super::ItemId) -> Result<i64> {
|
|
320 |
+ |
// SUM over many bigints widens to NUMERIC in Postgres; cap at i64::MAX
|
|
321 |
+ |
// before casting back to BIGINT so a pathological row count can't overflow
|
|
322 |
+ |
// the i64 the rest of the codebase passes around.
|
| 320 |
323 |
|
let total: i64 = sqlx::query_scalar(
|
| 321 |
|
- |
"SELECT COALESCE(SUM(file_size_bytes)::BIGINT, 0) FROM versions WHERE item_id = $1 AND file_size_bytes IS NOT NULL",
|
|
324 |
+ |
"SELECT COALESCE(LEAST(SUM(file_size_bytes), 9223372036854775807)::BIGINT, 0) FROM versions WHERE item_id = $1 AND file_size_bytes IS NOT NULL",
|
| 322 |
325 |
|
)
|
| 323 |
326 |
|
.bind(item_id)
|
| 324 |
327 |
|
.fetch_one(pool)
|
| 113 |
113 |
|
|
| 114 |
114 |
|
let session_layer = SessionManagerLayer::new(session_store)
|
| 115 |
115 |
|
.with_secure(secure_cookies)
|
|
116 |
+ |
.with_http_only(true)
|
| 116 |
117 |
|
.with_same_site(SameSite::Lax) // Lax allows session on top-level navigations (OAuth redirects)
|
| 117 |
118 |
|
.with_expiry(Expiry::OnInactivity(CookieDuration::days(
|
| 118 |
119 |
|
constants::SESSION_EXPIRY_DAYS,
|
| 70 |
70 |
|
auth: AuthUser,
|
| 71 |
71 |
|
axum::Form(form): axum::Form<SlugForm>,
|
| 72 |
72 |
|
) -> impl IntoResponse {
|
| 73 |
|
- |
if form.slug.len() < 2 {
|
|
73 |
+ |
if form.slug.is_empty() {
|
| 74 |
74 |
|
return Html(String::new());
|
| 75 |
75 |
|
}
|
|
76 |
+ |
if form.slug.len() < 2 {
|
|
77 |
+ |
return Html(
|
|
78 |
+ |
SaveStatusTemplate {
|
|
79 |
+ |
success: false,
|
|
80 |
+ |
message: "Must be at least 2 characters".to_string(),
|
|
81 |
+ |
}
|
|
82 |
+ |
.render_string(),
|
|
83 |
+ |
);
|
|
84 |
+ |
}
|
| 76 |
85 |
|
|
| 77 |
86 |
|
let slug = match Slug::new(&form.slug) {
|
| 78 |
87 |
|
Ok(s) => s,
|
| 119 |
128 |
|
auth: AuthUser,
|
| 120 |
129 |
|
axum::Form(form): axum::Form<SlugForm>,
|
| 121 |
130 |
|
) -> impl IntoResponse {
|
| 122 |
|
- |
if form.slug.len() < 2 {
|
|
131 |
+ |
if form.slug.is_empty() {
|
| 123 |
132 |
|
return Html(String::new());
|
| 124 |
133 |
|
}
|
|
134 |
+ |
if form.slug.len() < 2 {
|
|
135 |
+ |
return Html(
|
|
136 |
+ |
SaveStatusTemplate {
|
|
137 |
+ |
success: false,
|
|
138 |
+ |
message: "Must be at least 2 characters".to_string(),
|
|
139 |
+ |
}
|
|
140 |
+ |
.render_string(),
|
|
141 |
+ |
);
|
|
142 |
+ |
}
|
| 125 |
143 |
|
|
| 126 |
144 |
|
let slug = match Slug::new(&form.slug) {
|
| 127 |
145 |
|
Ok(s) => s,
|
| 152 |
170 |
|
auth: AuthUser,
|
| 153 |
171 |
|
axum::Form(form): axum::Form<BlogSlugForm>,
|
| 154 |
172 |
|
) -> impl IntoResponse {
|
| 155 |
|
- |
if form.slug.len() < 2 {
|
|
173 |
+ |
if form.slug.is_empty() {
|
| 156 |
174 |
|
return Html(String::new());
|
| 157 |
175 |
|
}
|
|
176 |
+ |
if form.slug.len() < 2 {
|
|
177 |
+ |
return Html(
|
|
178 |
+ |
SaveStatusTemplate {
|
|
179 |
+ |
success: false,
|
|
180 |
+ |
message: "Must be at least 2 characters".to_string(),
|
|
181 |
+ |
}
|
|
182 |
+ |
.render_string(),
|
|
183 |
+ |
);
|
|
184 |
+ |
}
|
| 158 |
185 |
|
|
| 159 |
186 |
|
let slug = match Slug::new(&form.slug) {
|
| 160 |
187 |
|
Ok(s) => s,
|
| 344 |
344 |
|
);
|
| 345 |
345 |
|
} else {
|
| 346 |
346 |
|
// No transaction found — check if this was a tip refund
|
| 347 |
|
- |
if db::tips::refund_tip_by_payment_intent(&state.db, payment_intent_id)
|
|
347 |
+ |
let tip_refunded = db::tips::refund_tip_by_payment_intent(&state.db, payment_intent_id)
|
| 348 |
348 |
|
.await
|
| 349 |
|
- |
.context("refund tip")?
|
| 350 |
|
- |
{
|
|
349 |
+ |
.inspect_err(|e| {
|
|
350 |
+ |
tracing::error!(
|
|
351 |
+ |
payment_intent_id = %payment_intent_id,
|
|
352 |
+ |
error = ?e,
|
|
353 |
+ |
"tip refund lookup failed"
|
|
354 |
+ |
);
|
|
355 |
+ |
})
|
|
356 |
+ |
.context("refund tip")?;
|
|
357 |
+ |
if tip_refunded {
|
| 351 |
358 |
|
tracing::info!(payment_intent_id = %payment_intent_id, "tip refund processed");
|
| 352 |
359 |
|
} else {
|
| 353 |
360 |
|
// No matching transaction or tip — the payment webhook likely hasn't
|