diff --git a/Cargo.lock b/Cargo.lock index 9c1707c..3262e0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,7 +607,7 @@ dependencies = [ [[package]] name = "fluffle" -version = "0.2.1" +version = "0.3.0" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index 23a5455..030acff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fluffle" -version = "0.2.1" +version = "0.3.0" edition = "2024" authors = ["trisuaso"] repository = "https://trisua.com/t/fluffle" diff --git a/app/templates_src/edit.lisp b/app/templates_src/edit.lisp index fc6ea85..21ab13e 100644 --- a/app/templates_src/edit.lisp +++ b/app/templates_src/edit.lisp @@ -43,7 +43,7 @@ ("style" "margin-top: var(--pad-2)") ("onsubmit" "edit_entry(event)") (div - ("class" "flex gap-2") + ("class" "w-full flex gap-2") (input ("class" "w-full") ("type" "text") @@ -51,12 +51,22 @@ ("name" "edit_code") ("required" "") ("placeholder" "Enter edit code")) + (input ("class" "w-full") ("style" "visibility: hidden") ("aria-hidden" "true") ("disabled" "true")) + (input ("class" "w-full") ("style" "visibility: hidden") ("aria-hidden" "true") ("disabled" "true"))) + (div + ("class" "flex gap-2") (input ("class" "w-full") ("type" "text") ("minlength" "2") ("name" "new_edit_code") ("placeholder" "New edit code")) + (input + ("class" "w-full") + ("type" "text") + ("minlength" "2") + ("name" "new_modify_code") + ("placeholder" "New modify code")) (input ("class" "w-full") ("type" "text") @@ -116,6 +126,7 @@ edit_code: e.target.edit_code.value, new_slug: e.target.new_slug.value || undefined, new_edit_code: e.target.new_edit_code.value || undefined, + new_modify_code: e.target.new_modify_code.value || undefined, metadata: globalThis.metadata_editor.getValue(), \"delete\": rm, }), diff --git a/src/model.rs b/src/model.rs index 166d908..58b90fc 100644 --- a/src/model.rs +++ b/src/model.rs @@ -16,6 +16,9 @@ pub struct Entry { /// The IP address of the last editor of the entry. #[serde(default)] pub last_edit_from: String, + /// An edit code that can only be used to change the entry's content. + #[serde(default)] + pub modify_code: String, } #[derive(Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/routes.rs b/src/routes.rs index 3d408a8..1f1bb14 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -492,6 +492,7 @@ async fn create_request( content: req.content, metadata: req.metadata, last_edit_from: real_ip, + modify_code: String::new(), }) .unwrap(), ) @@ -533,6 +534,8 @@ struct EditEntry { #[serde(default)] new_edit_code: Option, #[serde(default)] + new_modify_code: Option, + #[serde(default)] metadata: String, #[serde(default)] delete: bool, @@ -601,116 +604,136 @@ async fn edit_request( Err(e) => return Json(e.into()), }; + let edit_code = hash(req.edit_code.clone() + &entry.salt); + let using_modify_code = edit_code == entry.modify_code; + // check edit code - if hash(req.edit_code.clone() + &entry.salt) != entry.edit_code { + if edit_code + != *if using_modify_code { + &entry.modify_code + } else { + &entry.edit_code + } + { return Json(Error::NotAllowed.into()); } - // handle delete - if req.delete { - let views_id = match data - .query(&SimplifiedQuery { - query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)), - mode: AppDataSelectMode::One(0), - }) - .await - { - Ok(r) => match r { - AppDataQueryResult::One(r) => r.id, - AppDataQueryResult::Many(_) => unreachable!(), - }, - Err(e) => return Json(e.into()), - }; + // ... + if !using_modify_code { + // handle delete + if req.delete { + let views_id = match data + .query(&SimplifiedQuery { + query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)), + mode: AppDataSelectMode::One(0), + }) + .await + { + Ok(r) => match r { + AppDataQueryResult::One(r) => r.id, + AppDataQueryResult::Many(_) => unreachable!(), + }, + Err(e) => return Json(e.into()), + }; - return match data.remove(id).await { - Ok(_) => match data.remove(views_id).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: None, - }), + return match data.remove(id).await { + Ok(_) => match data.remove(views_id).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: None, + }), + Err(e) => Json(e.into()), + }, Err(e) => Json(e.into()), - }, - Err(e) => Json(e.into()), - }; - } - - // check edited slug and edit code - if let Some(mut new_slug) = req.new_slug { - new_slug = new_slug.to_lowercase(); - - if new_slug.len() < 2 { - return Json(Error::DataTooShort("slug".to_string()).into()); + }; } - if new_slug.len() > 32 { - return Json(Error::DataTooLong("slug".to_string()).into()); + // check edited slug and edit code + if let Some(mut new_slug) = req.new_slug { + new_slug = new_slug.to_lowercase(); + + if new_slug.len() < 2 { + return Json(Error::DataTooShort("slug".to_string()).into()); + } + + if new_slug.len() > 32 { + return Json(Error::DataTooLong("slug".to_string()).into()); + } + + // check slug + let regex = regex::RegexBuilder::new(NAME_REGEX) + .multi_line(true) + .build() + .unwrap(); + + if regex.captures(&new_slug).is_some() { + return Json( + Error::MiscError("This slug contains invalid characters".to_string()).into(), + ); + } + + // check for existing + if data + .query(&SimplifiedQuery { + query: AppDataSelectQuery::KeyIs(format!("entries('{}')", new_slug)), + mode: AppDataSelectMode::One(0), + }) + .await + .is_ok() + { + return Json(Error::MiscError("Slug already in use".to_string()).into()); + } + + let views_id = match data + .query(&SimplifiedQuery { + query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)), + mode: AppDataSelectMode::One(0), + }) + .await + { + Ok(r) => match r { + AppDataQueryResult::One(r) => r.id, + AppDataQueryResult::Many(_) => unreachable!(), + }, + Err(e) => return Json(e.into()), + }; + + // rename + if let Err(e) = data.rename(id, format!("entries('{}')", new_slug)).await { + return Json(e.into()); + } + + if let Err(e) = data + .rename(views_id, format!("entries.views('{}')", new_slug)) + .await + { + return Json(e.into()); + } + + entry.slug = new_slug; } - // check slug - let regex = regex::RegexBuilder::new(NAME_REGEX) - .multi_line(true) - .build() - .unwrap(); - - if regex.captures(&new_slug).is_some() { - return Json( - Error::MiscError("This slug contains invalid characters".to_string()).into(), - ); + if let Some(new_edit_code) = req.new_edit_code { + entry.salt = salt(); + entry.edit_code = hash(new_edit_code + &entry.salt); } - // check for existing - if data - .query(&SimplifiedQuery { - query: AppDataSelectQuery::KeyIs(format!("entries('{}')", new_slug)), - mode: AppDataSelectMode::One(0), - }) - .await - .is_ok() - { - return Json(Error::MiscError("Slug already in use".to_string()).into()); + // update modify code + if let Some(new_modify_code) = req.new_modify_code { + entry.modify_code = hash(new_modify_code + &entry.salt); } - - let views_id = match data - .query(&SimplifiedQuery { - query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)), - mode: AppDataSelectMode::One(0), - }) - .await - { - Ok(r) => match r { - AppDataQueryResult::One(r) => r.id, - AppDataQueryResult::Many(_) => unreachable!(), - }, - Err(e) => return Json(e.into()), - }; - - // rename - if let Err(e) = data.rename(id, format!("entries('{}')", new_slug)).await { - return Json(e.into()); - } - - if let Err(e) = data - .rename(views_id, format!("entries.views('{}')", new_slug)) - .await - { - return Json(e.into()); - } - - entry.slug = new_slug; - } - - if let Some(new_edit_code) = req.new_edit_code { - entry.salt = salt(); - entry.edit_code = hash(new_edit_code + &entry.salt); } // update entry.content = req.content; - entry.metadata = req.metadata; - entry.last_edit_from = real_ip; entry.edited = unix_epoch_timestamp(); + if !using_modify_code { + entry.metadata = req.metadata; + entry.last_edit_from = real_ip; + } + if let Err(e) = data .update(id, serde_json::to_string(&entry).unwrap()) .await