diff --git a/src/db.rs b/src/db.rs index 65d3cb8..76d5080 100644 --- a/src/db.rs +++ b/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 { + 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(()) +} diff --git a/src/handlers.rs b/src/handlers.rs index 3f8a7ca..58ba81f 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -288,6 +288,65 @@ pub async fn subscribe_to_user( } } +pub async fn unsubscribe_from_user( + State(state): State, + headers: HeaderMap, + Form(form): Form>, +) -> Result { + 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, headers: HeaderMap, @@ -1419,6 +1478,12 @@ fn parse_entry_form_fields(form: &HashMap) -> 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) -> 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, "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)) } } diff --git a/src/main.rs b/src/main.rs index 5d87c3c..f074785 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,10 @@ async fn main() -> Result<(), Box> { ) .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)) diff --git a/templates/day.html b/templates/day.html index 7b39b54..c435473 100644 --- a/templates/day.html +++ b/templates/day.html @@ -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" /> diff --git a/templates/planning.html b/templates/planning.html index 022b235..26cee68 100644 --- a/templates/planning.html +++ b/templates/planning.html @@ -26,6 +26,7 @@ No subscriptions yet.

{% else %} -
+
{% for username in page.subscriptions %} - - @{{ username }} - +
+ + @{{ username }} + +
+ + +
+
{% endfor %}
{% endif %}