diff --git a/Cargo.lock b/Cargo.lock index 3262e0a..b05f860 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,7 +571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -623,7 +623,7 @@ dependencies = [ "tetratto-core", "tetratto-shared", "tokio", - "toml 0.9.2", + "toml 0.9.4", "tower-http", "tracing", "tracing-subscriber", @@ -997,7 +997,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -1951,7 +1951,7 @@ dependencies = [ "pin-project-lite", "ryu", "sha1_smol", - "socket2", + "socket2 0.5.10", "tokio", "tokio-util", "url", @@ -2082,7 +2082,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2205,9 +2205,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -2397,6 +2397,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2513,7 +2523,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2571,7 +2581,7 @@ dependencies = [ "tetratto-l10n", "tetratto-shared", "tokio", - "toml 0.9.2", + "toml 0.9.4", "totp-rs", ] @@ -2583,7 +2593,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86" dependencies = [ "pathbufd", "serde", - "toml 0.9.2", + "toml 0.9.4", ] [[package]] @@ -2711,9 +2721,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -2723,9 +2733,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2769,7 +2779,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.9.1", - "socket2", + "socket2 0.5.10", "tokio", "tokio-util", "whoami", @@ -2824,9 +2834,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" dependencies = [ "indexmap", "serde", @@ -3385,7 +3395,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 030acff..ad1dcb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ homepage = "https://fluffle.cc" [dependencies] tetratto-core = "12.0.2" tetratto-shared = "12.0.6" -tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } pathbufd = "0.1.4" serde = { version = "1.0.219", features = ["derive"] } tera = "1.20.0" @@ -27,7 +27,7 @@ axum-extra = { version = "0.10.1", features = ["cookie"] } nanoneo = "0.2.0" dotenv = "0.15.0" glob = "0.3.2" -serde_json = "1.0.141" -toml = "0.9.2" +serde_json = "1.0.142" +toml = "0.9.4" serde_valid = { version = "1.0.5", features = ["toml"] } regex = "1.11.1" diff --git a/README.md b/README.md index f21eea0..0e72c79 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Once you've built the binary, it'll be located at (from the root `fluffle/` dire All templates are compiled with [nanoneo](https://trisua.com/t/nanoneo), so it's recommended that you familiarize yourself with that syntax. +You can set a master password to be able to freely edit all entries by using the `MASTER_PASS` environment variable. + ## Attribution Fluffle is licensed under the AGPL-3.0 license. Tetratto is also licensed under the AGPL-3.0 license. diff --git a/app/public/fluffle_bunny.webp b/app/public/fluffle_bunny.webp new file mode 100644 index 0000000..41f20ea Binary files /dev/null and b/app/public/fluffle_bunny.webp differ diff --git a/app/public/style.css b/app/public/style.css index b50ca6e..6183231 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -301,7 +301,7 @@ input { border-left: solid 0px transparent; } -input:focus { +input:not([type="checkbox"]):focus { outline: solid 2px var(--color-primary); box-shadow: 0 0 0 4px oklch(87% 0.065 274.039 / 25%); background: var(--color-super-raised); @@ -316,6 +316,10 @@ input.surface { background: var(--color-surface); } +input[type="checkbox"] { + height: max-content; +} + /* typo */ p { margin-bottom: var(--pad-4); diff --git a/app/templates_src/view.lisp b/app/templates_src/view.lisp index 1f63ccf..be5cd15 100644 --- a/app/templates_src/view.lisp +++ b/app/templates_src/view.lisp @@ -61,6 +61,14 @@ (button ("class" "button camo fade no_fill") ("title" "Toggle high-contrast") + ("id" "toggle_high_contrast_button") ("onclick" "toggle_metadata_css(event)") (text "{{ icon \"contrast\" }}")) + +(text "{% if \"EntryHighContrast\" in flags -%}") +(script + (text "setTimeout(() => { + toggle_metadata_css({ target: document.getElementById(\"toggle_high_contrast_button\") }); + }, 150);")) +(text "{%- endif %}") (text "{% endblock %}") diff --git a/app/templates_src/warning.lisp b/app/templates_src/warning.lisp new file mode 100644 index 0000000..62bc611 --- /dev/null +++ b/app/templates_src/warning.lisp @@ -0,0 +1,54 @@ +(text "{% extends \"root.lisp\" %} {% block head %}") +(text "{% if not metadata.page_title -%}") +(title + (text "{{ entry.slug }}")) +(text "{%- endif %} {{ metadata_head|safe }}") + +(text "{% if not metadata.share_title -%}") +(meta ("property" "og:title") ("content" "{{ entry.slug }}")) +(meta ("property" "twitter:title") ("content" "{{ entry.slug }}")) +(text "{%- endif %}") + +(text "{% if metadata.page_icon|length == 0 -%}") +(link ("rel" "icon") ("href" "/public/favicon.svg")) +(text "{%- endif %}") + +(text "{% endblock %} {% block body %}") +(div + ("class" "card container flex flex-col gap-1") + ("id" "content_rect") + (p ("class" "fade") (text "Content warning:")) + (div (text "{{ metadata.safety_content_warning|markdown|safe }}")) + (hr) + (div + ("class" "flex flex-col gap-4") + (label + ("class" "flex flex-row gap-2 items-center") + ("for" "open_in_high_contrast") + (input + ("type" "checkbox") + ("id" "open_in_high_contrast") + ("name" "open_in_high_contrast")) + (span (text "Open in high contrast"))) + (div + ("class" "flex gap-2") + (button + ("class" "button surface green") + ("onclick" "accept()") + (text "Continue")) + (button + ("class" "button surface red") + ("onclick" "window.history.back()") + (text "Cancel"))))) + +(script + (text "const QFLAGS = [\"AcceptWarning\"]; + function accept() { + if (document.getElementById(\"open_in_high_contrast\").checked) { + QFLAGS.push(\"EntryHighContrast\"); + } + + document.cookie = `Atto-QFlags=\"${JSON.stringify(QFLAGS)}\"; path=/`; + window.location.reload(); + }")) +(text "{% endblock %}") diff --git a/src/model.rs b/src/model.rs index 58b90fc..ad8a348 100644 --- a/src/model.rs +++ b/src/model.rs @@ -342,6 +342,7 @@ pub struct EntryMetadata { pub content_font_weight: u32, /// The text size of elements (separated by space). #[serde(default, alias = "CONTENT_TEXT_SIZE")] + #[validate(max_length = 128)] pub content_text_size: String, /// The text size of elements by element tag. /// @@ -357,13 +358,19 @@ pub struct EntryMetadata { pub content_text_align: TextAlignment, /// The base text color. #[serde(default, alias = "CONTENT_TEXT_COLOR")] + #[validate(max_length = 128)] pub content_text_color: String, /// The color of links. #[serde(default, alias = "CONTENT_LINK_COLOR")] + #[validate(max_length = 128)] pub content_link_color: String, /// If paragraph elements have a margin below them. #[serde(default, alias = "CONTENT_DISABLE_PARAGRAPH_MARGIN")] pub content_disable_paragraph_margin: bool, + /// The content warning shown before viewing this entry. + #[serde(default, alias = "SAFETY_CONTENT_WARNING")] + #[validate(max_length = 512)] + pub safety_content_warning: String, } macro_rules! metadata_css { @@ -684,3 +691,15 @@ impl EntryMetadata { output } } + +/// Flags that can be provided through the "Atto-QFlags" cookie to customize the +/// resulting page. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum QuickFlag { + /// If the entry's warning is automatically skipped. + AcceptWarning, + /// If the entry is automatically set to high-contrast mode. + EntryHighContrast, +} + +pub type QuickFlags = Vec; diff --git a/src/routes.rs b/src/routes.rs index 650476d..64029f6 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -2,7 +2,7 @@ use std::env::var; use crate::{ State, - model::{Entry, EntryMetadata}, + model::{Entry, EntryMetadata, QuickFlag, QuickFlags}, }; use axum::{ Extension, Json, Router, @@ -126,11 +126,20 @@ pub struct ViewQuery { } async fn view_request( + jar: CookieJar, Extension(data): Extension, Path(mut slug): Path, Query(props): Query, ) -> impl IntoResponse { let (ref data, ref tera, ref build_code) = *data.read().await; + let qflags: QuickFlags = match jar.get("Atto-QFlags") { + Some(x) => match serde_json::from_str(&x.value_trimmed()) { + Ok(x) => x, + Err(_) => QuickFlags::default(), + }, + None => QuickFlags::default(), + }; + slug = slug.to_lowercase(); let entry = match data @@ -222,8 +231,15 @@ async fn view_request( ctx.insert("metadata_head", &metadata.head_tags()); ctx.insert("metadata_css", &metadata.css()); ctx.insert("password", &props.key); + ctx.insert("flags", &qflags); - Html(tera.render("view.lisp", &ctx).unwrap()) + if metadata.safety_content_warning.is_empty() | qflags.contains(&QuickFlag::AcceptWarning) { + // regular view + Html(tera.render("view.lisp", &ctx).unwrap()) + } else { + // warning + Html(tera.render("warning.lisp", &ctx).unwrap()) + } } async fn editor_request( @@ -614,12 +630,20 @@ async fn edit_request( let using_modify_code = edit_code == entry.modify_code; // check edit code - if edit_code - != *if using_modify_code { - &entry.modify_code - } else { - &entry.edit_code + let mut using_master = false; + if let Ok(master_pass) = var("MASTER_PASS") { + if req.edit_code == master_pass { + using_master = true; } + } + + if !using_master + && edit_code + != *if using_modify_code { + &entry.modify_code + } else { + &entry.edit_code + } { return Json(Error::NotAllowed.into()); }