max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+81 insertions,
-21 deletions
| @@ -4862,6 +4862,14 @@ | |||
| 4862 | 4862 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4863 | 4863 | checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" | |
| 4864 | 4864 | ||
| 4865 | + | [[patch.unused]] | |
| 4866 | + | name = "synckit-client" | |
| 4867 | + | version = "0.8.0" | |
| 4868 | + | ||
| 4869 | + | [[patch.unused]] | |
| 4870 | + | name = "synckit-config" | |
| 4871 | + | version = "0.2.0" | |
| 4872 | + | ||
| 4865 | 4873 | [[patch.unused]] | |
| 4866 | 4874 | name = "kberg" | |
| 4867 | 4875 | version = "0.1.0" | |
| @@ -4893,11 +4901,3 @@ | |||
| 4893 | 4901 | [[patch.unused]] | |
| 4894 | 4902 | name = "quasi-webview" | |
| 4895 | 4903 | version = "0.1.0" | |
| 4896 | - | ||
| 4897 | - | [[patch.unused]] | |
| 4898 | - | name = "synckit-client" | |
| 4899 | - | version = "0.8.0" | |
| 4900 | - | ||
| 4901 | - | [[patch.unused]] | |
| 4902 | - | name = "synckit-config" | |
| 4903 | - | version = "0.2.0" |
| @@ -21,10 +21,10 @@ | |||
| 21 | 21 | use mt_core::types::{ChatPolicy, CommunityState, ModAction, ModActor}; | |
| 22 | 22 | ||
| 23 | 23 | use super::{ | |
| 24 | - | ChatSettingsForm, CreateCategoryForm, CreateTagForm, DeleteTagForm, EditCategoryFormData, | |
| 25 | - | MoveCategoryForm, SetCommunityStateForm, UpdateCommunityForm, audit, begin_tx, commit_tx, | |
| 26 | - | db_error, field_error, is_platform_admin, parse_uuid, require_mod_or_superadmin, require_owner, | |
| 27 | - | template_user, validate_title, | |
| 24 | + | ChatSettingsForm, CleanSlateForm, CreateCategoryForm, CreateTagForm, DeleteTagForm, | |
| 25 | + | EditCategoryFormData, MoveCategoryForm, SetCommunityStateForm, UpdateCommunityForm, audit, | |
| 26 | + | begin_tx, commit_tx, db_error, field_error, is_platform_admin, parse_uuid, | |
| 27 | + | require_mod_or_superadmin, require_owner, template_user, validate_title, | |
| 28 | 28 | }; | |
| 29 | 29 | ||
| 30 | 30 | #[tracing::instrument(skip_all)] | |
| @@ -221,9 +221,20 @@ | |||
| 221 | 221 | axum::extract::State(state): axum::extract::State<AppState>, | |
| 222 | 222 | Path(slug): Path<String>, | |
| 223 | 223 | RequireUser(user): RequireUser, | |
| 224 | + | Form(form): Form<CleanSlateForm>, | |
| 224 | 225 | ) -> Result<Redirect, Response> { | |
| 225 | 226 | require_owner(&state, &slug, &user).await?; | |
| 226 | 227 | ||
| 228 | + | // Typed-phrase confirmation, the same check and the same form the admin | |
| 229 | + | // clean slate uses. Trim only; case is significant. Checked here rather | |
| 230 | + | // than in the browser because a client-side dialog is a suggestion. | |
| 231 | + | if form.confirm.trim() != slug { | |
| 232 | + | return Err(field_error( | |
| 233 | + | "confirm", | |
| 234 | + | "Confirmation phrase did not match the community slug.", | |
| 235 | + | )); | |
| 236 | + | } | |
| 237 | + | ||
| 227 | 238 | let room = MtChatRooms::new(state.db.clone()) | |
| 228 | 239 | .resolve(&slug) | |
| 229 | 240 | .await |
| @@ -74,18 +74,25 @@ | |||
| 74 | 74 | </form> | |
| 75 | 75 | </div> | |
| 76 | 76 | ||
| 77 | - | {# No typed-slug confirmation, unlike the admin clean slate. That one | |
| 78 | - | destroys threads written to be permanent; this brings forward a | |
| 79 | - | deletion the retention window was always going to perform, and the | |
| 80 | - | room says so on the tin. #} | |
| 77 | + | {# Typed-slug confirmation, the same shape the admin clean slate uses. | |
| 78 | + | A button alone is one misclick away from destroying everyone's | |
| 79 | + | messages, and the server checks the phrase rather than trusting a | |
| 80 | + | dialog the client could skip. #} | |
| 81 | 81 | <div class="danger-zone"> | |
| 82 | 82 | <p class="form-help"> | |
| 83 | 83 | Delete every message in the room right now, rather than waiting for the | |
| 84 | 84 | retention window. <strong>This cannot be undone.</strong> It is recorded in | |
| 85 | 85 | the moderation log. | |
| 86 | 86 | </p> | |
| 87 | - | <form method="post" action="/p/{{ community_slug }}/settings/chat/wipe"> | |
| 87 | + | <p class="form-help"> | |
| 88 | + | To confirm, type the community slug <code>{{ community_slug }}</code> below. | |
| 89 | + | </p> | |
| 90 | + | <form method="post" action="/p/{{ community_slug }}/settings/chat/wipe" class="form-container"> | |
| 88 | 91 | {% include "partials/csrf_input.html" %} | |
| 92 | + | <div class="form-group"> | |
| 93 | + | <label for="wipe_confirm">Confirmation</label> | |
| 94 | + | <input type="text" id="wipe_confirm" name="confirm" placeholder="{{ community_slug }}" autocomplete="off" required> | |
| 95 | + | </div> | |
| 89 | 96 | <div class="form-actions"> | |
| 90 | 97 | <button type="submit" class="danger">Delete all chat messages now</button> | |
| 91 | 98 | </div> |
| @@ -237,7 +237,10 @@ | |||
| 237 | 237 | .unwrap(); | |
| 238 | 238 | } | |
| 239 | 239 | ||
| 240 | - | let resp = h.client.post_form("/p/test/settings/chat/wipe", "").await; | |
| 240 | + | let resp = h | |
| 241 | + | .client | |
| 242 | + | .post_form("/p/test/settings/chat/wipe", "confirm=test") | |
| 243 | + | .await; | |
| 241 | 244 | assert_eq!(resp.status, StatusCode::SEE_OTHER); | |
| 242 | 245 | ||
| 243 | 246 | assert!( | |
| @@ -277,7 +280,10 @@ | |||
| 277 | 280 | .post_form("/p/test/settings/chat", &form("off", "168", "5000")) | |
| 278 | 281 | .await; | |
| 279 | 282 | ||
| 280 | - | let resp = h.client.post_form("/p/test/settings/chat/wipe", "").await; | |
| 283 | + | let resp = h | |
| 284 | + | .client | |
| 285 | + | .post_form("/p/test/settings/chat/wipe", "confirm=test") | |
| 286 | + | .await; | |
| 281 | 287 | assert_eq!(resp.status, StatusCode::SEE_OTHER); | |
| 282 | 288 | assert!( | |
| 283 | 289 | mt_db::queries::recent_backlog(&h.db, id, 10) | |
| @@ -308,7 +314,10 @@ | |||
| 308 | 314 | sign_in(&mut h, moderator, "mod").await; | |
| 309 | 315 | h.client.get("/p/test/chat").await; | |
| 310 | 316 | ||
| 311 | - | let resp = h.client.post_form("/p/test/settings/chat/wipe", "").await; | |
| 317 | + | let resp = h | |
| 318 | + | .client | |
| 319 | + | .post_form("/p/test/settings/chat/wipe", "confirm=test") | |
| 320 | + | .await; | |
| 312 | 321 | ||
| 313 | 322 | assert_eq!(resp.status, StatusCode::FORBIDDEN); | |
| 314 | 323 | assert_eq!( | |
| @@ -321,6 +330,39 @@ | |||
| 321 | 330 | ); | |
| 322 | 331 | } | |
| 323 | 332 | ||
| 333 | + | #[sqlx::test] | |
| 334 | + | async fn a_wipe_without_the_typed_slug_does_nothing(_pool: sqlx::PgPool) { | |
| 335 | + | // The misclick guard. Checked server-side because a confirmation the client | |
| 336 | + | // owns is a suggestion, and this destroys everyone's messages at once. | |
| 337 | + | let mut h = TestHarness::new().await; | |
| 338 | + | let (id, owner) = owned_community(&mut h).await; | |
| 339 | + | h.client | |
| 340 | + | .post_form("/p/test/settings/chat", &form("members", "168", "5000")) | |
| 341 | + | .await; | |
| 342 | + | mt_db::mutations::insert_chat_message(&h.db, id, owner, "hi", 168) | |
| 343 | + | .await | |
| 344 | + | .unwrap(); | |
| 345 | + | ||
| 346 | + | // Case is significant, so the community's own name in title case is not it. | |
| 347 | + | for body in ["confirm=", "confirm=Test", "confirm=wrong"] { | |
| 348 | + | let resp = h.client.post_form("/p/test/settings/chat/wipe", body).await; | |
| 349 | + | assert_eq!( | |
| 350 | + | resp.status, | |
| 351 | + | StatusCode::UNPROCESSABLE_ENTITY, | |
| 352 | + | "{body:?} should not have been accepted" | |
| 353 | + | ); | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | assert_eq!( | |
| 357 | + | mt_db::queries::recent_backlog(&h.db, id, 10) | |
| 358 | + | .await | |
| 359 | + | .unwrap() | |
| 360 | + | .len(), | |
| 361 | + | 1, | |
| 362 | + | "the room is intact" | |
| 363 | + | ); | |
| 364 | + | } | |
| 365 | + | ||
| 324 | 366 | #[sqlx::test] | |
| 325 | 367 | async fn wiping_without_a_csrf_token_is_refused(_pool: sqlx::PgPool) { | |
| 326 | 368 | let mut h = TestHarness::new().await; | |
| @@ -336,7 +378,7 @@ | |||
| 336 | 378 | ||
| 337 | 379 | let resp = h | |
| 338 | 380 | .client | |
| 339 | - | .post_form_no_csrf("/p/test/settings/chat/wipe", "") | |
| 381 | + | .post_form_no_csrf("/p/test/settings/chat/wipe", "confirm=test") | |
| 340 | 382 | .await; | |
| 341 | 383 | ||
| 342 | 384 | assert_eq!(resp.status, StatusCode::FORBIDDEN); |