max / makenotwork
5 files changed,
+71 insertions,
-20 deletions
| @@ -344,21 +344,25 @@ pub async fn create_category( | |||
| 344 | 344 | Ok(row.0) | |
| 345 | 345 | } | |
| 346 | 346 | ||
| 347 | - | /// Update a category's name and description. | |
| 347 | + | /// Update a category's name and description (scoped to community). | |
| 348 | 348 | #[tracing::instrument(skip_all)] | |
| 349 | 349 | pub async fn update_category( | |
| 350 | 350 | pool: &PgPool, | |
| 351 | 351 | category_id: Uuid, | |
| 352 | + | community_id: Uuid, | |
| 352 | 353 | name: &str, | |
| 353 | 354 | description: Option<&str>, | |
| 354 | - | ) -> Result<(), sqlx::Error> { | |
| 355 | - | sqlx::query("UPDATE categories SET name = $2, description = $3 WHERE id = $1") | |
| 356 | - | .bind(category_id) | |
| 357 | - | .bind(name) | |
| 358 | - | .bind(description) | |
| 359 | - | .execute(pool) | |
| 360 | - | .await?; | |
| 361 | - | Ok(()) | |
| 355 | + | ) -> Result<bool, sqlx::Error> { | |
| 356 | + | let result = sqlx::query( | |
| 357 | + | "UPDATE categories SET name = $2, description = $3 WHERE id = $1 AND community_id = $4", | |
| 358 | + | ) | |
| 359 | + | .bind(category_id) | |
| 360 | + | .bind(name) | |
| 361 | + | .bind(description) | |
| 362 | + | .bind(community_id) | |
| 363 | + | .execute(pool) | |
| 364 | + | .await?; | |
| 365 | + | Ok(result.rows_affected() > 0) | |
| 362 | 366 | } | |
| 363 | 367 | ||
| 364 | 368 | /// Swap the sort_order of two categories atomically. | |
| @@ -661,14 +665,15 @@ pub async fn create_tag( | |||
| 661 | 665 | Ok(row.0) | |
| 662 | 666 | } | |
| 663 | 667 | ||
| 664 | - | /// Delete a tag (CASCADE removes thread_tags rows). | |
| 668 | + | /// Delete a tag (CASCADE removes thread_tags rows), scoped to community. | |
| 665 | 669 | #[tracing::instrument(skip_all)] | |
| 666 | - | pub async fn delete_tag(pool: &PgPool, tag_id: Uuid) -> Result<(), sqlx::Error> { | |
| 667 | - | sqlx::query("DELETE FROM tags WHERE id = $1") | |
| 670 | + | pub async fn delete_tag(pool: &PgPool, tag_id: Uuid, community_id: Uuid) -> Result<bool, sqlx::Error> { | |
| 671 | + | let result = sqlx::query("DELETE FROM tags WHERE id = $1 AND community_id = $2") | |
| 668 | 672 | .bind(tag_id) | |
| 673 | + | .bind(community_id) | |
| 669 | 674 | .execute(pool) | |
| 670 | 675 | .await?; | |
| 671 | - | Ok(()) | |
| 676 | + | Ok(result.rows_affected() > 0) | |
| 672 | 677 | } | |
| 673 | 678 | ||
| 674 | 679 | /// Set the tags for a thread (delete existing + batch insert new, in a transaction). |
| @@ -1319,6 +1319,28 @@ pub async fn has_user_flagged_post( | |||
| 1319 | 1319 | .await | |
| 1320 | 1320 | } | |
| 1321 | 1321 | ||
| 1322 | + | /// Check if a flag belongs to a given community (via post → thread → category chain). | |
| 1323 | + | #[tracing::instrument(skip_all)] | |
| 1324 | + | pub async fn flag_belongs_to_community( | |
| 1325 | + | pool: &PgPool, | |
| 1326 | + | flag_id: Uuid, | |
| 1327 | + | community_id: Uuid, | |
| 1328 | + | ) -> Result<bool, sqlx::Error> { | |
| 1329 | + | sqlx::query_scalar( | |
| 1330 | + | "SELECT EXISTS( | |
| 1331 | + | SELECT 1 FROM post_flags pf | |
| 1332 | + | JOIN posts p ON p.id = pf.post_id | |
| 1333 | + | JOIN threads t ON t.id = p.thread_id | |
| 1334 | + | JOIN categories c ON c.id = t.category_id | |
| 1335 | + | WHERE pf.id = $1 AND c.community_id = $2 | |
| 1336 | + | )", | |
| 1337 | + | ) | |
| 1338 | + | .bind(flag_id) | |
| 1339 | + | .bind(community_id) | |
| 1340 | + | .fetch_one(pool) | |
| 1341 | + | .await | |
| 1342 | + | } | |
| 1343 | + | ||
| 1322 | 1344 | ||
| 1323 | 1345 | // ============================================================================ | |
| 1324 | 1346 | // Endorsement queries |
| @@ -107,9 +107,20 @@ pub(super) async fn dismiss_flag_handler( | |||
| 107 | 107 | let user = session_user | |
| 108 | 108 | .ok_or_else(|| Redirect::to("/auth/login").into_response())?; | |
| 109 | 109 | ||
| 110 | - | let (_community, _role) = require_mod_or_owner(&state, &slug, &user).await?; | |
| 110 | + | let (community, _role) = require_mod_or_owner(&state, &slug, &user).await?; | |
| 111 | 111 | let flag_id = parse_uuid(&flag_id_str)?; | |
| 112 | 112 | ||
| 113 | + | // Verify flag belongs to this community before acting on it | |
| 114 | + | let flag_exists = mt_db::queries::flag_belongs_to_community(&state.db, flag_id, community.id) | |
| 115 | + | .await | |
| 116 | + | .map_err(|e| { | |
| 117 | + | tracing::error!(error = ?e, "db error checking flag community"); | |
| 118 | + | (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() | |
| 119 | + | })?; | |
| 120 | + | if !flag_exists { | |
| 121 | + | return Err((StatusCode::NOT_FOUND, "Not found").into_response()); | |
| 122 | + | } | |
| 123 | + | ||
| 113 | 124 | mt_db::mutations::resolve_flag(&state.db, flag_id, user.user_id, "dismissed") | |
| 114 | 125 | .await | |
| 115 | 126 | .map_err(|e| { | |
| @@ -136,11 +147,17 @@ pub(super) async fn remove_flagged_post_handler( | |||
| 136 | 147 | let (community, _role) = require_mod_or_owner(&state, &slug, &user).await?; | |
| 137 | 148 | let flag_id = parse_uuid(&flag_id_str)?; | |
| 138 | 149 | ||
| 139 | - | // Get the flag to find the post_id | |
| 150 | + | // Get the flag to find the post_id — scoped to this community | |
| 140 | 151 | let flag_row: Option<(uuid::Uuid, uuid::Uuid)> = sqlx::query_as( | |
| 141 | - | "SELECT post_id, (SELECT author_id FROM posts WHERE id = post_flags.post_id) FROM post_flags WHERE id = $1", | |
| 152 | + | "SELECT pf.post_id, p.author_id | |
| 153 | + | FROM post_flags pf | |
| 154 | + | JOIN posts p ON p.id = pf.post_id | |
| 155 | + | JOIN threads t ON t.id = p.thread_id | |
| 156 | + | JOIN categories c ON c.id = t.category_id | |
| 157 | + | WHERE pf.id = $1 AND c.community_id = $2", | |
| 142 | 158 | ) | |
| 143 | 159 | .bind(flag_id) | |
| 160 | + | .bind(community.id) | |
| 144 | 161 | .fetch_optional(&state.db) | |
| 145 | 162 | .await | |
| 146 | 163 | .map_err(|e| { |
| @@ -253,12 +253,15 @@ pub(super) async fn edit_category_handler( | |||
| 253 | 253 | } | |
| 254 | 254 | let desc_opt = if description.is_empty() { None } else { Some(description) }; | |
| 255 | 255 | ||
| 256 | - | mt_db::mutations::update_category(&state.db, cat_id, name, desc_opt) | |
| 256 | + | let updated = mt_db::mutations::update_category(&state.db, cat_id, community.id, name, desc_opt) | |
| 257 | 257 | .await | |
| 258 | 258 | .map_err(|e| { | |
| 259 | 259 | tracing::error!(error = ?e, "db error updating category"); | |
| 260 | 260 | StatusCode::INTERNAL_SERVER_ERROR.into_response() | |
| 261 | 261 | })?; | |
| 262 | + | if !updated { | |
| 263 | + | return Err(StatusCode::NOT_FOUND.into_response()); | |
| 264 | + | } | |
| 262 | 265 | ||
| 263 | 266 | log_mod_action( | |
| 264 | 267 | &state.db, Some(community.id), user.user_id, | |
| @@ -369,16 +372,19 @@ pub(super) async fn delete_tag_handler( | |||
| 369 | 372 | let user = session_user | |
| 370 | 373 | .ok_or_else(|| Redirect::to("/auth/login").into_response())?; | |
| 371 | 374 | ||
| 372 | - | let _community = require_owner(&state, &slug, &user).await?; | |
| 375 | + | let community = require_owner(&state, &slug, &user).await?; | |
| 373 | 376 | ||
| 374 | 377 | let tag_id = parse_uuid(&form.tag_id)?; | |
| 375 | 378 | ||
| 376 | - | mt_db::mutations::delete_tag(&state.db, tag_id) | |
| 379 | + | let deleted = mt_db::mutations::delete_tag(&state.db, tag_id, community.id) | |
| 377 | 380 | .await | |
| 378 | 381 | .map_err(|e| { | |
| 379 | 382 | tracing::error!(error = ?e, "db error deleting tag"); | |
| 380 | 383 | StatusCode::INTERNAL_SERVER_ERROR.into_response() | |
| 381 | 384 | })?; | |
| 385 | + | if !deleted { | |
| 386 | + | return Err(StatusCode::NOT_FOUND.into_response()); | |
| 387 | + | } | |
| 382 | 388 | ||
| 383 | 389 | Ok(Redirect::to(&format!( | |
| 384 | 390 | "/p/{slug}/settings?toast=Tag+deleted" |
| @@ -238,9 +238,10 @@ async fn update_category_updates_fields() { | |||
| 238 | 238 | .await | |
| 239 | 239 | .unwrap(); | |
| 240 | 240 | ||
| 241 | - | mt_db::mutations::update_category(&h.db, cat_id, "New Name", Some("New desc")) | |
| 241 | + | let updated = mt_db::mutations::update_category(&h.db, cat_id, comm_id, "New Name", Some("New desc")) | |
| 242 | 242 | .await | |
| 243 | 243 | .unwrap(); | |
| 244 | + | assert!(updated); | |
| 244 | 245 | ||
| 245 | 246 | let (name, desc): (String, Option<String>) = sqlx::query_as( | |
| 246 | 247 | "SELECT name, description FROM categories WHERE id = $1", |