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};
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
|
|
||||||
|
pub const MAX_CALORIES_VALUE: i64 = 1_000_000;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FoodEntry {
|
pub struct FoodEntry {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|
@ -82,7 +84,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
entry_date TEXT NOT NULL,
|
entry_date TEXT NOT NULL,
|
||||||
name 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
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_food_entries_user_date
|
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 (
|
CREATE TABLE IF NOT EXISTS user_planning (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
target_weight REAL,
|
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),
|
bmr REAL CHECK (bmr > 0),
|
||||||
public_entries INTEGER NOT NULL DEFAULT 0,
|
public_entries INTEGER NOT NULL DEFAULT 0,
|
||||||
public_weights 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,6 +249,19 @@ pub fn create_subscription(
|
||||||
Ok(changed > 0)
|
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(
|
pub fn fetch_subscription_targets(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
subscriber_user_id: i64,
|
subscriber_user_id: i64,
|
||||||
|
|
@ -695,3 +712,19 @@ fn table_has_column(
|
||||||
}
|
}
|
||||||
Ok(false)
|
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(
|
pub async fn show_inbox(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|
@ -1419,6 +1478,12 @@ fn parse_entry_form_fields(form: &HashMap<String, String>) -> Result<(String, i6
|
||||||
if calories < 0 {
|
if calories < 0 {
|
||||||
return Err((StatusCode::BAD_REQUEST, "Calories must be >= 0".to_string()));
|
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))
|
Ok((name.to_string(), calories))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1449,6 +1514,15 @@ fn parse_new_entry_form_fields(form: &HashMap<String, String>) -> Result<(String
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"Calories total is too large".to_string(),
|
"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))
|
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(),
|
"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))
|
Ok(Some(parsed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
)
|
)
|
||||||
.route("/subscribe", get(handlers::show_subscribe))
|
.route("/subscribe", get(handlers::show_subscribe))
|
||||||
.route("/subscribe", post(handlers::subscribe_to_user))
|
.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}", get(handlers::show_public_profile))
|
||||||
.route("/u/{username}/day/{date}", get(handlers::show_public_day))
|
.route("/u/{username}/day/{date}", get(handlers::show_public_day))
|
||||||
.route("/reports", get(handlers::show_reports))
|
.route("/reports", get(handlers::show_reports))
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
name="calories"
|
name="calories"
|
||||||
placeholder="Calories each"
|
placeholder="Calories each"
|
||||||
min="0"
|
min="0"
|
||||||
|
max="1000000"
|
||||||
required
|
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"
|
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"
|
name="calories"
|
||||||
value="{{ entry.calories }}"
|
value="{{ entry.calories }}"
|
||||||
min="0"
|
min="0"
|
||||||
|
max="1000000"
|
||||||
required
|
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"
|
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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
|
max="1000000"
|
||||||
name="target_calories"
|
name="target_calories"
|
||||||
value="{{ page.target_calories_value }}"
|
value="{{ page.target_calories_value }}"
|
||||||
placeholder="e.g. 2200"
|
placeholder="e.g. 2200"
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,25 @@
|
||||||
{% if page.subscriptions.is_empty() %}
|
{% if page.subscriptions.is_empty() %}
|
||||||
<p class="mt-2 text-sm text-slate-600">No subscriptions yet.</p>
|
<p class="mt-2 text-sm text-slate-600">No subscriptions yet.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<div class="mt-3 grid gap-2">
|
||||||
{% for username in page.subscriptions %}
|
{% for username in page.subscriptions %}
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<a
|
<a
|
||||||
href="/u/{{ username }}"
|
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"
|
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 }}
|
@{{ username }}
|
||||||
</a>
|
</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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue