integer overflow fixes
This commit is contained in:
parent
97412a8589
commit
5f50b77676
37
src/db.rs
37
src/db.rs
|
|
@ -2,6 +2,8 @@ use std::collections::HashMap;
|
|||
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
|
||||
pub const MAX_CALORIES_VALUE: i64 = 1_000_000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FoodEntry {
|
||||
pub id: i64,
|
||||
|
|
@ -82,7 +84,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||
user_id INTEGER NOT NULL,
|
||||
entry_date TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
calories INTEGER NOT NULL CHECK (calories >= 0),
|
||||
calories INTEGER NOT NULL CHECK (calories >= 0 AND calories <= 1000000),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_food_entries_user_date
|
||||
|
|
@ -100,7 +102,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||
CREATE TABLE IF NOT EXISTS user_planning (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
target_weight REAL,
|
||||
target_calories INTEGER CHECK (target_calories >= 0),
|
||||
target_calories INTEGER CHECK (target_calories >= 0 AND target_calories <= 1000000),
|
||||
bmr REAL CHECK (bmr > 0),
|
||||
public_entries INTEGER NOT NULL DEFAULT 0,
|
||||
public_weights INTEGER NOT NULL DEFAULT 0,
|
||||
|
|
@ -137,6 +139,8 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||
[],
|
||||
)?;
|
||||
}
|
||||
|
||||
sanitize_calorie_values(conn, MAX_CALORIES_VALUE)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +249,19 @@ pub fn create_subscription(
|
|||
Ok(changed > 0)
|
||||
}
|
||||
|
||||
pub fn delete_subscription(
|
||||
conn: &Connection,
|
||||
subscriber_user_id: i64,
|
||||
target_user_id: i64,
|
||||
) -> Result<bool, rusqlite::Error> {
|
||||
let changed = conn.execute(
|
||||
"DELETE FROM subscriptions
|
||||
WHERE subscriber_user_id = ?1 AND target_user_id = ?2",
|
||||
params![subscriber_user_id, target_user_id],
|
||||
)?;
|
||||
Ok(changed > 0)
|
||||
}
|
||||
|
||||
pub fn fetch_subscription_targets(
|
||||
conn: &Connection,
|
||||
subscriber_user_id: i64,
|
||||
|
|
@ -695,3 +712,19 @@ fn table_has_column(
|
|||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn sanitize_calorie_values(conn: &Connection, max_calories: i64) -> Result<(), rusqlite::Error> {
|
||||
conn.execute(
|
||||
"UPDATE user_food_entries
|
||||
SET calories = ?1
|
||||
WHERE calories > ?1",
|
||||
params![max_calories],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE user_planning
|
||||
SET target_calories = ?1
|
||||
WHERE target_calories > ?1",
|
||||
params![max_calories],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -288,6 +288,65 @@ pub async fn subscribe_to_user(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn unsubscribe_from_user(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<HashMap<String, String>>,
|
||||
) -> Result<Response, AppError> {
|
||||
let auth = get_auth_user(&state, &headers).await?;
|
||||
let Some(auth) = auth else {
|
||||
return Ok(Redirect::to("/login").into_response());
|
||||
};
|
||||
|
||||
let username = form.get("username").map(|v| v.trim()).unwrap_or("");
|
||||
if username.is_empty() {
|
||||
return render_subscribe_page_with_messages(
|
||||
&state,
|
||||
&auth,
|
||||
"",
|
||||
"Username is required",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let target = {
|
||||
let db_conn = state.db.lock().await;
|
||||
db::fetch_user_by_username(&db_conn, username).map_err(internal_db_error)?
|
||||
};
|
||||
let Some(target) = target else {
|
||||
return render_subscribe_page_with_messages(
|
||||
&state,
|
||||
&auth,
|
||||
"",
|
||||
"User not found",
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
let removed = {
|
||||
let db_conn = state.db.lock().await;
|
||||
db::delete_subscription(&db_conn, auth.user.id, target.id).map_err(internal_db_error)?
|
||||
};
|
||||
|
||||
if removed {
|
||||
render_subscribe_page_with_messages(
|
||||
&state,
|
||||
&auth,
|
||||
&format!("Unsubscribed from @{}", target.username),
|
||||
"",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
render_subscribe_page_with_messages(
|
||||
&state,
|
||||
&auth,
|
||||
"",
|
||||
&format!("You were not subscribed to @{}", target.username),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn show_inbox(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
|
|
@ -1419,6 +1478,12 @@ fn parse_entry_form_fields(form: &HashMap<String, String>) -> Result<(String, i6
|
|||
if calories < 0 {
|
||||
return Err((StatusCode::BAD_REQUEST, "Calories must be >= 0".to_string()));
|
||||
}
|
||||
if calories > db::MAX_CALORIES_VALUE {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Calories must be <= {}", db::MAX_CALORIES_VALUE),
|
||||
));
|
||||
}
|
||||
Ok((name.to_string(), calories))
|
||||
}
|
||||
|
||||
|
|
@ -1449,6 +1514,15 @@ fn parse_new_entry_form_fields(form: &HashMap<String, String>) -> Result<(String
|
|||
StatusCode::BAD_REQUEST,
|
||||
"Calories total is too large".to_string(),
|
||||
))?;
|
||||
if total > db::MAX_CALORIES_VALUE {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!(
|
||||
"Calories total must be <= {}",
|
||||
db::MAX_CALORIES_VALUE
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok((name, total))
|
||||
}
|
||||
|
||||
|
|
@ -1494,6 +1568,12 @@ fn parse_optional_non_negative_i64(raw: Option<&String>) -> Result<Option<i64>,
|
|||
"Target calories must be >= 0".to_string(),
|
||||
));
|
||||
}
|
||||
if parsed > db::MAX_CALORIES_VALUE {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Target calories must be <= {}", db::MAX_CALORIES_VALUE),
|
||||
));
|
||||
}
|
||||
Ok(Some(parsed))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
)
|
||||
.route("/subscribe", get(handlers::show_subscribe))
|
||||
.route("/subscribe", post(handlers::subscribe_to_user))
|
||||
.route(
|
||||
"/subscribe/unsubscribe",
|
||||
post(handlers::unsubscribe_from_user),
|
||||
)
|
||||
.route("/u/{username}", get(handlers::show_public_profile))
|
||||
.route("/u/{username}/day/{date}", get(handlers::show_public_day))
|
||||
.route("/reports", get(handlers::show_reports))
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
name="calories"
|
||||
placeholder="Calories each"
|
||||
min="0"
|
||||
max="1000000"
|
||||
required
|
||||
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
||||
/>
|
||||
|
|
@ -90,6 +91,7 @@
|
|||
name="calories"
|
||||
value="{{ entry.calories }}"
|
||||
min="0"
|
||||
max="1000000"
|
||||
required
|
||||
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000000"
|
||||
name="target_calories"
|
||||
value="{{ page.target_calories_value }}"
|
||||
placeholder="e.g. 2200"
|
||||
|
|
|
|||
|
|
@ -30,14 +30,25 @@
|
|||
{% if page.subscriptions.is_empty() %}
|
||||
<p class="mt-2 text-sm text-slate-600">No subscriptions yet.</p>
|
||||
{% else %}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<div class="mt-3 grid gap-2">
|
||||
{% for username in page.subscriptions %}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a
|
||||
href="/u/{{ username }}"
|
||||
class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"
|
||||
>
|
||||
@{{ username }}
|
||||
</a>
|
||||
<form method="post" action="/subscribe/unsubscribe">
|
||||
<input type="hidden" name="username" value="{{ username }}" />
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg border border-rose-300 bg-rose-50 px-3 py-2 text-sm font-semibold text-rose-700 hover:bg-rose-100"
|
||||
>
|
||||
Unsubscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue