diff --git a/.gitignore b/.gitignore index fb3ddd3..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ /target -app/fluffle.toml -migration.js diff --git a/Cargo.lock b/Cargo.lock index 76c7ea5..a7eb6bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,9 +243,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -542,9 +542,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "emojis" -version = "0.7.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52f3d011046a013bdefbc63a5523b06ad0c0f1e227941baf98475496229d634" +checksum = "0a08afd8e599463c275703532e707c767b8c068a826eea9ca8fceaf3435029df" dependencies = [ "phf 0.12.1", ] @@ -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]] @@ -607,14 +607,13 @@ dependencies = [ [[package]] name = "fluffle" -version = "1.0.0" +version = "0.4.0" dependencies = [ "axum", "axum-extra", "dotenv", "glob", "nanoneo", - "oiseau", "pathbufd", "regex", "serde", @@ -624,7 +623,7 @@ dependencies = [ "tetratto-core", "tetratto-shared", "tokio", - "toml 0.9.5", + "toml 0.9.4", "tower-http", "tracing", "tracing-subscriber", @@ -810,7 +809,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "ignore", "walkdir", ] @@ -1182,7 +1181,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "cfg-if", "libc", ] @@ -1495,7 +1494,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -1832,7 +1831,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -1964,7 +1963,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", ] [[package]] @@ -2013,9 +2012,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -2079,11 +2078,11 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2167,7 +2166,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -2499,7 +2498,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -2524,7 +2523,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2562,14 +2561,14 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "15.0.1" +version = "12.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aeb9dcc5631ec6188bb9438dc97015c6662b6f59e650e5afa865775f170c9c" +checksum = "a367ac3ced8ff302080e1b4a82a67acd24fa606245c4381a6f77dbaaf6ef4b58" dependencies = [ "async-recursion", "base16ct", "base64", - "bitflags 2.9.2", + "bitflags 2.9.1", "emojis", "md-5", "oiseau", @@ -2582,7 +2581,7 @@ dependencies = [ "tetratto-l10n", "tetratto-shared", "tokio", - "toml 0.9.5", + "toml 0.9.4", "totp-rs", ] @@ -2594,7 +2593,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86" dependencies = [ "pathbufd", "serde", - "toml 0.9.5", + "toml 0.9.4", ] [[package]] @@ -2835,9 +2834,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" dependencies = [ "indexmap", "serde", @@ -2882,9 +2881,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" dependencies = [ "winnow", ] @@ -2940,7 +2939,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", "bytes", "futures-core", "futures-util", @@ -3396,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]] @@ -3638,7 +3637,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1d91342..47aa4fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fluffle" -version = "1.0.0" +version = "0.4.0" edition = "2024" authors = ["trisuaso"] repository = "https://trisua.com/t/fluffle" @@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later" homepage = "https://fluffle.cc" [dependencies] -tetratto-core = "15.0.1" +tetratto-core = "12.0.2" tetratto-shared = "12.0.6" tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } pathbufd = "0.1.4" @@ -31,4 +31,3 @@ serde_json = "1.0.142" toml = "0.9.4" serde_valid = { version = "1.0.5", features = ["toml"] } regex = "1.11.1" -oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] } diff --git a/README.md b/README.md index fd98dfd..0e72c79 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,27 @@ Fluffle is a familiar Markdown pastebin-y site :) +Since Tetratto is used as a backend, you'll obviously need to create an app at . Once you've created the app, scroll down to "Secret key" and roll the key. Copy the key since you'll need it for later. + ## Usage Once you've cloned the repository, cd into the `app` directory and run `cargo run -r`. -After you start the server the first time, a `fluffle.toml` file will be created in the current directory. You'll need to edit that file to configure your PostgreSQL connection, instance name/theme color, etc. +Before you run the server, however, you should create a `.env` file to store your API key (that you rolled earlier). In the `.env` file, put the following: + +```ini +API_KEY= +``` + +So if your key was "ABCD123", you would have: + +```ini +API_KEY=ABCD123 +``` + +Once this file is in place, you can safely run the server. You can also optionally add a `PORT` variable in there to change the port number. If you don't change the port, you can find the server at `http://localhost:9119`. + +It's important to note that you're fairly limited on app storage without the Tetratto developer pass. You can manage your billing settings at . ## Customization @@ -22,7 +38,7 @@ 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` configuration key. A password is automatically generated on first start as well. Please note that this value is stored in plain text. +You can set a master password to be able to freely edit all entries by using the `MASTER_PASS` environment variable. ## Attribution diff --git a/app/public/style.css b/app/public/style.css index 5f7e506..52323f1 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -283,14 +283,6 @@ video { position: absolute; z-index: 2; top: 100%; - right: 0; - width: max-content; - max-width: 15rem; -} - -.dropdown .inner.left { - right: unset; - left: 0; } .dropdown .inner.open { @@ -817,7 +809,7 @@ dialog { border: 0; } -dialog .inner { +dialog.inner { display: flex; flex-direction: column; gap: var(--pad-2); @@ -831,23 +823,3 @@ dialog::backdrop { dialog:is(.dark *)::backdrop { background: hsla(0, 0%, 100%, 15%); } - -/* menus */ -menu { - display: flex; -} - -menu .button { - justify-content: flex-start; - width: 100%; -} - -menu .button.active { - background: var(--color-super-raised); -} - -menu.col { - flex-direction: column; - width: 25rem; - max-width: 100%; -} diff --git a/app/templates_src/claim.lisp b/app/templates_src/claim.lisp index b81816a..bff9a83 100644 --- a/app/templates_src/claim.lisp +++ b/app/templates_src/claim.lisp @@ -6,7 +6,7 @@ (div ("class" "card container") (h1 (text "{{ entry.slug }}")) - (p (text "Custom slug reclaims are handled through ") (b (text "{{ config.service_hosts.tetratto }}")) (text ". You'll need to have an account there to submit a claim request.")) + (p (text "Custom slug reclaims are handled through ") (b (text "{{ tetratto }}")) (text ". You'll need to have an account there to submit a claim request.")) (p (text "Please note that you are unlikely to receive a response unless your claim is accepted. Please do not submit additional requests for the same slug.")) (text "{% if metadata.tetratto_owner_username -%}") @@ -24,13 +24,13 @@ (text "{% if metadata.tetratto_owner_username -%}") ; contact owner button (a - ("href" "{{ config.service_hosts.tetratto }}/mail/compose?receivers={{ metadata.tetratto_owner_username }}&subject=Reclaim%20for%20%22{{ entry.slug }}%22") + ("href" "{{ tetratto }}/mail/compose?receivers={{ tetratto_owner_username }}&subject=Reclaim%20for%20%22{{ entry.slug }}%22") ("class" "button surface no_fill") (text "{{ icon \"external-link\" }} Contact owner")) (text "{%- endif %}") (a - ("href" "{{ config.service_hosts.tetratto }}/mail/compose?receivers={{ tetratto_handler_account_username }}&subject=Reclaim%20for%20%22{{ entry.slug }}%22") + ("href" "{{ tetratto }}/mail/compose?receivers={{ tetratto_handler_account_username }}&subject=Reclaim%20for%20%22{{ entry.slug }}%22") ("class" "button surface no_fill") (text "{{ icon \"external-link\" }} Submit request")) (text "{% else %}") diff --git a/app/templates_src/edit.lisp b/app/templates_src/edit.lisp index 0edfdfb..adde02e 100644 --- a/app/templates_src/edit.lisp +++ b/app/templates_src/edit.lisp @@ -143,7 +143,7 @@ const { load, failed } = submitter_load(rm ? document.getElementById(\"fake_delete_button\") : e.submitter); load(); - fetch(\"/api/v1/entries/{{ entry.id }}\", { + fetch(\"/api/v1/entries/{{ entry.slug }}\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", diff --git a/app/templates_src/root.lisp b/app/templates_src/root.lisp index 7e985b8..12eb5b9 100644 --- a/app/templates_src/root.lisp +++ b/app/templates_src/root.lisp @@ -6,7 +6,7 @@ (meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0")) (meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge")) - (link ("rel" "stylesheet") ("href" "https://repodelivery.tetratto.com/tetratto/crates/app/src/public/css/utility.css")) + (link ("rel" "stylesheet") ("href" "{{ tetratto }}/css/utility.css?v={{ build_code }}")) (link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}")) (style (text ":root { --color-primary: {{ theme_color }}; }")) @@ -33,7 +33,7 @@ ("class" "button camo fade") (text "{{ icon \"menu\" }}")) (div - ("class" "inner left") + ("class" "inner") (a ("class" "button") ("href" "/") diff --git a/app/templates_src/view.lisp b/app/templates_src/view.lisp index 6c4816c..5dff22c 100644 --- a/app/templates_src/view.lisp +++ b/app/templates_src/view.lisp @@ -46,18 +46,16 @@ (text "Owner:") (a ("class" "flex items_center gap_2") - ("href" "{{ config.service_hosts.tetratto }}/@{{ metadata.tetratto_owner_username }}") - (text "{% if metadata.tetratto_owner_id -%}") + ("href" "{{ tetratto }}/@{{ metadata.tetratto_owner_username }}") (img ("class" "avatar") - ("src" "{{ config.service_hosts.buckets }}/avatars/{{ metadata.tetratto_owner_id }}")) - (text "{%- endif %}") + ("src" "{{ tetratto }}/api/v1/auth/user/{{ metadata.tetratto_owner_username }}/avatar?selector_type=username")) (text "{{ metadata.tetratto_owner_username }}"))) (text "{%- endif %}") ; views (text "{% if not metadata.option_disable_views -%}") - (span (text "Views: {{ entry.views }}")) + (span (text "Views: {{ views }}")) (text "{%- endif %}") ; easy-to-read diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 8f57438..0000000 --- a/src/config.rs +++ /dev/null @@ -1,115 +0,0 @@ -use oiseau::config::{Configuration, DatabaseConfig}; -use pathbufd::PathBufD; -use serde::{Deserialize, Serialize}; -use tetratto_shared::hash::random_id; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ServiceHostsConfig { - pub tetratto: String, - pub buckets: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Config { - /// The name of the site. Shown in the UI. - #[serde(default = "default_name")] - pub name: String, - /// The (CSS) theme color of the site. Shown in the UI. - #[serde(default = "default_theme_color")] - pub theme_color: String, - /// The slug of the instance's information page. - /// - /// Should be the pathname WITHOUT the leading slash. - #[serde(default = "default_what_page_slug")] - pub what_page_slug: String, - /// The username of the handler account in charge of this instance on the - /// linked Tetratto host. - #[serde(default = "default_tetratto_handler_account_username")] - pub tetratto_handler_account_username: String, - /// Database configuration. - #[serde(default = "default_database")] - pub database: DatabaseConfig, - /// Real IP header (for reverse proxy). - #[serde(default = "default_real_ip_header")] - pub real_ip_header: String, - /// The host URL of required services. - #[serde(default = "default_service_hosts")] - pub service_hosts: ServiceHostsConfig, - /// The master password which is allowed to do anything without password checks. - pub master_pass: String, -} - -fn default_name() -> String { - "Fluffle".to_string() -} - -fn default_theme_color() -> String { - "#a3b3ff".to_string() -} - -fn default_what_page_slug() -> String { - "what".to_string() -} - -fn default_tetratto_handler_account_username() -> String { - "fluffle".to_string() -} - -fn default_database() -> DatabaseConfig { - DatabaseConfig::default() -} - -fn default_real_ip_header() -> String { - "CF-Connecting-IP".to_string() -} - -fn default_service_hosts() -> ServiceHostsConfig { - ServiceHostsConfig { - tetratto: "https://tetratto.com".to_string(), - buckets: "https://assetdelivery.tetratto.com".to_string(), - } -} - -impl Configuration for Config { - fn db_config(&self) -> DatabaseConfig { - self.database.to_owned() - } -} - -impl Default for Config { - fn default() -> Self { - Self { - name: default_name(), - theme_color: default_theme_color(), - what_page_slug: default_what_page_slug(), - tetratto_handler_account_username: default_tetratto_handler_account_username(), - database: default_database(), - real_ip_header: default_real_ip_header(), - service_hosts: default_service_hosts(), - master_pass: random_id(), - } - } -} - -impl Config { - /// Read the configuration file. - pub fn read() -> Self { - toml::from_str( - &match std::fs::read_to_string(PathBufD::current().join("fluffle.toml")) { - Ok(x) => x, - Err(_) => { - let x = Config::default(); - - std::fs::write( - PathBufD::current().join("fluffle.toml"), - &toml::to_string_pretty(&x).expect("failed to serialize config"), - ) - .expect("failed to write config"); - - return x; - } - }, - ) - .expect("failed to deserialize config") - } -} diff --git a/src/database/entries.rs b/src/database/entries.rs deleted file mode 100644 index 8776a14..0000000 --- a/src/database/entries.rs +++ /dev/null @@ -1,316 +0,0 @@ -use super::{DataManager, NAME_REGEX}; -use crate::model::{Entry, EntryMetadata}; -use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_row}; -use serde_valid::Validate; -use tetratto_core::{ - auto_method, - model::{Error, Result}, -}; -use tetratto_shared::{hash::hash, unix_epoch_timestamp}; - -impl DataManager { - /// Get an [`Entry`] from an SQL row. - pub(crate) fn get_entry_from_row(x: &PostgresRow) -> Entry { - Entry { - id: get!(x->0(i64)) as usize, - slug: get!(x->1(String)), - edit_code: get!(x->2(String)), - salt: get!(x->3(String)), - created: get!(x->4(i64)) as usize, - edited: get!(x->5(i64)) as usize, - content: get!(x->6(String)), - metadata: get!(x->7(String)), - last_edit_from: get!(x->8(String)), - modify_code: get!(x->9(String)), - views: get!(x->10(i64)) as usize, - } - } - - auto_method!(get_entry_by_id(usize as i64)@get_entry_from_row -> "SELECT * FROM entries WHERE id = $1" --name="entry" --returns=Entry --cache-key-tmpl="fluf.entry:{}"); - auto_method!(get_entry_by_slug(&str)@get_entry_from_row -> "SELECT * FROM entries WHERE slug = $1" --name="entry" --returns=Entry --cache-key-tmpl="fluf.entry:{}"); - - fn hash_passwords(metadata: &mut EntryMetadata) -> (bool, String) { - // hash passwords - let do_update_metadata = (!metadata.option_view_password.is_empty() - || !metadata.option_source_password.is_empty()) - && (!metadata.option_view_password.starts_with("h:") - || !metadata.option_source_password.starts_with("h:")); - - if !metadata.option_view_password.is_empty() - && !metadata.option_view_password.starts_with("h:") - { - metadata.option_view_password = - format!("h:{}", hash(metadata.option_view_password.clone())); - } - - if !metadata.option_source_password.is_empty() - && !metadata.option_source_password.starts_with("h:") - { - metadata.option_source_password = - format!("h:{}", hash(metadata.option_source_password.clone())); - } - - if do_update_metadata { - if let Ok(x) = toml::to_string_pretty(&metadata) { - return (true, x); - }; - } - - (false, String::new()) - } - - /// Create a new entry in the database. - /// - /// # Arguments - /// * `data` - a mock [`Entry`] object to insert - pub async fn create_entry(&self, mut data: Entry) -> Result { - // check values - if data.slug.trim().len() < 2 { - return Err(Error::DataTooShort("slug".to_string())); - } else if data.slug.len() > 128 { - return Err(Error::DataTooLong("slug".to_string())); - } - - if data.content.len() < 2 { - return Err(Error::DataTooShort("content".to_string())); - } - - if data.content.len() > 150_000 { - return Err(Error::DataTooLong("content".to_string())); - } - - // check characters - let regex = regex::RegexBuilder::new(NAME_REGEX) - .multi_line(true) - .build() - .unwrap(); - - if regex.captures(&data.slug).is_some() { - return Err(Error::MiscError( - "This slug contains invalid characters".to_string(), - )); - } - - // check for existing - if self.get_entry_by_slug(&data.slug).await.is_ok() { - return Err(Error::MiscError("Slug is already in use".to_string())); - } - - // check metadata - let mut metadata: EntryMetadata = - match toml::from_str(&EntryMetadata::ini_to_toml(&data.metadata)) { - Ok(x) => x, - Err(e) => return Err(Error::MiscError(e.to_string())), - }; - - if let Err(e) = metadata.validate() { - return Err(Error::MiscError(e.to_string())); - } - - let (do_update_metadata, updated) = Self::hash_passwords(&mut metadata); - if do_update_metadata { - data.metadata = updated; - } - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "INSERT INTO entries VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", - params![ - &(data.id as i64), - &data.slug, - &data.edit_code, - &data.salt, - &(data.created as i64), - &(data.edited as i64), - &data.content, - &data.metadata, - &data.last_edit_from, - &data.modify_code, - &(data.views as i64) - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(data) - } - - /// Update an existing entry. - pub async fn update_entry( - &self, - id: usize, - edit_code: String, - mut new_slug: String, - new_content: String, - mut new_metadata: String, - mut new_edit_code: String, - mut new_modify_code: String, - by_ip: String, - ) -> Result { - // check values - if !new_slug.is_empty() { - if new_slug.trim().len() < 2 { - return Err(Error::DataTooShort("slug".to_string())); - } else if new_slug.len() > 128 { - return Err(Error::DataTooLong("slug".to_string())); - } - } - - if new_content.len() < 2 { - return Err(Error::DataTooShort("content".to_string())); - } - - if new_content.len() > 150_000 { - return Err(Error::DataTooLong("content".to_string())); - } - - // check characters - let regex = regex::RegexBuilder::new(NAME_REGEX) - .multi_line(true) - .build() - .unwrap(); - - if regex.captures(&new_slug).is_some() { - return Err(Error::MiscError( - "This slug contains invalid characters".to_string(), - )); - } - - // check metadata - let mut metadata: EntryMetadata = - match toml::from_str(&EntryMetadata::ini_to_toml(&new_metadata)) { - Ok(x) => x, - Err(e) => return Err(Error::MiscError(e.to_string())), - }; - - if let Err(e) = metadata.validate() { - return Err(Error::MiscError(e.to_string())); - } - - let (do_update_metadata, updated) = Self::hash_passwords(&mut metadata); - if do_update_metadata { - new_metadata = updated; - } - - // get stored version of entry - let entry = self.get_entry_by_id(id).await?; - - // check password - let using_modify = hash(edit_code.clone() + &entry.salt) == entry.modify_code; - - if !using_modify && edit_code != self.0.0.master_pass { - if !entry.check_password(edit_code) { - return Err(Error::NotAllowed); - } - } - - // remove cached - self.cache_clear_entry(&entry).await; - - // hash junk - if !using_modify { - if new_slug.is_empty() { - // use original; no change - new_slug = entry.slug; - } else { - // make sure slug is all lowercase - new_slug = new_slug.to_lowercase(); - } - - if !new_edit_code.is_empty() { - new_edit_code = hash(new_edit_code + &entry.salt); - } else { - // use original; no change - new_edit_code = entry.edit_code; - } - - if !new_modify_code.is_empty() { - new_modify_code = hash(new_modify_code + &entry.salt); - } else { - // use original; no change - new_modify_code = entry.modify_code; - } - } else { - // using modify code; no change - new_slug = entry.slug; - new_edit_code = entry.edit_code; - new_modify_code = entry.modify_code; - } - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "UPDATE entries SET slug = $1, edit_code = $2, modify_code = $3, content = $4, metadata = $5, edited = $6, last_edit_from = $7 WHERE id = $8", - params![ - &new_slug, - &new_edit_code, - &new_modify_code, - &new_content, - &new_metadata, - &(unix_epoch_timestamp() as i64), - &by_ip, - &(id as i64), - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(new_slug) - } - - /// Delete an existing entry. - pub async fn delete_entry(&self, id: usize, edit_code: String) -> Result<()> { - // get entry - let entry = self.get_entry_by_id(id).await?; - - // check password - if edit_code != self.0.0.master_pass { - if !entry.check_password(edit_code) { - return Err(Error::NotAllowed); - } - } - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "DELETE FROM entries WHERE id = $1", - params![&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.cache_clear_entry(&entry).await; - Ok(()) - } - - /// Remove an [`Entry`] from the cache. - pub async fn cache_clear_entry(&self, entry: &Entry) -> bool { - self.0.1.remove(format!("fluf.entry:{}", entry.id)).await - && self.0.1.remove(format!("fluf.entry:{}", entry.slug)).await - } - - auto_method!(incr_entry_views()@get_entry_by_id -> "UPDATE entries SET views = views + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_entry --incr); - auto_method!(decr_entry_views()@get_entry_by_id -> "UPDATE entries SET views = views - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_entry --decr=views); -} diff --git a/src/database/mod.rs b/src/database/mod.rs deleted file mode 100644 index 78f93bb..0000000 --- a/src/database/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod entries; -mod sql; - -use crate::config::Config; -use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult}; -use tetratto_core::model::{Error, Result}; - -pub const NAME_REGEX: &str = r"[^\w_\-\.,!]+"; - -#[derive(Clone)] -pub struct DataManager(pub OiseauManager); - -impl DataManager { - /// Create a new [`DataManager`]. - pub async fn new(config: Config) -> PgResult { - Ok(Self(OiseauManager::new(config).await?)) - } - - /// Initialize tables. - pub async fn init(&self) -> Result<()> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - execute!(&conn, sql::CREATE_TABLE_ENTRIES).unwrap(); - - Ok(()) - } -} diff --git a/src/database/sql/create_entries.sql b/src/database/sql/create_entries.sql deleted file mode 100644 index fcf0db5..0000000 --- a/src/database/sql/create_entries.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE IF NOT EXISTS entries ( - id BIGINT NOT NULL PRIMARY KEY, - slug TEXT NOT NULL, - edit_code TEXT NOT NULL, - salt TEXT NOT NULL, - created BIGINT NOT NULL, - edited BIGINT NOT NULL, - content TEXT NOT NULL, - metadata TEXT NOT NULL, - last_edit_from TEXT NOT NULL, - modify_code TEXT NOT NULL, - views BIGINT NOT NULL -) diff --git a/src/database/sql/mod.rs b/src/database/sql/mod.rs deleted file mode 100644 index 3a33cce..0000000 --- a/src/database/sql/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub const CREATE_TABLE_ENTRIES: &str = include_str!("./create_entries.sql"); diff --git a/src/main.rs b/src/main.rs index f215ddc..e93e886 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,13 @@ #![doc = include_str!("../README.md")] -mod config; -mod database; mod markdown; mod model; mod routes; -use crate::database::DataManager; use axum::{Extension, Router}; -use config::Config; use nanoneo::core::element::Render; use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc}; use tera::{Tera, Value}; -use tetratto_core::html; +use tetratto_core::{html, sdk::DataClient}; use tetratto_shared::hash::salt; use tokio::sync::RwLock; use tower_http::{ @@ -20,7 +16,7 @@ use tower_http::{ }; use tracing::{Level, info}; -pub(crate) type InnerState = (DataManager, Tera, String); +pub(crate) type InnerState = (DataClient, Tera, String); pub(crate) type State = Arc>; fn render_markdown(value: &Value, _: &HashMap) -> tera::Result { @@ -61,10 +57,10 @@ async fn main() { }; // ... - let database = DataManager::new(Config::read()) - .await - .expect("failed to connect to database"); - database.init().await.expect("failed to init database"); + let database = DataClient::new( + Some(var("TETRATTO").unwrap_or("https://tetratto.com".to_string())), + var("API_KEY").expect("API_KEY environment variable required"), + ); // build lisp create_dir_if_not_exists!("./templates_build"); diff --git a/src/model.rs b/src/model.rs index 641eb40..0351f2b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,15 +2,9 @@ use crate::markdown::is_numeric; use serde::{Deserialize, Serialize}; use serde_valid::Validate; use std::fmt::Display; -use tetratto_shared::{ - hash::{hash, salt}, - snow::Snowflake, - unix_epoch_timestamp, -}; #[derive(Serialize, Deserialize)] pub struct Entry { - pub id: usize, pub slug: String, pub edit_code: String, pub salt: String, @@ -25,42 +19,6 @@ pub struct Entry { /// An edit code that can only be used to change the entry's content. #[serde(default)] pub modify_code: String, - #[serde(default)] - pub views: usize, -} - -impl Entry { - /// Create a new [`Entry`]. - pub fn new( - slug: String, - edit_code: String, - content: String, - metadata: String, - last_edit_from: String, - ) -> Self { - let salt = salt(); - let edit_code = hash(edit_code.clone() + &salt); - let created = unix_epoch_timestamp(); - - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - slug, - edit_code, - salt, - created, - edited: created, - content, - metadata, - last_edit_from, - modify_code: String::new(), - views: 0, - } - } - - /// Check the given password against the entry's stored password hash. - pub fn check_password(&self, supplied: String) -> bool { - hash(supplied + &self.salt) == self.edit_code - } } #[derive(Serialize, Deserialize, PartialEq, Eq)] @@ -444,9 +402,6 @@ pub struct EntryMetadata { #[serde(default, alias = "TETRATTO_OWNER_USERNAME")] #[validate(max_length = 32)] pub tetratto_owner_username: String, - /// The ID of the owner of this entry on the Tetratto instance. - #[serde(default, alias = "TETRATTO_OWNER_ID")] - pub tetratto_owner_id: usize, } macro_rules! metadata_css { @@ -603,40 +558,6 @@ impl EntryMetadata { input.replace("}", "").replace(";", "").replace("/*", "") } - /// Split the given input string by the given character while skipping over - /// CSS colors. - pub fn css_color_split(c: char, input: &str) -> Vec { - let mut out = Vec::new(); - let mut buffer = String::new(); - let mut in_function = false; - - for x in input.chars() { - if x == c && !in_function { - out.push(buffer.clone()); - buffer.clear(); - continue; - } - - match x { - '(' => { - in_function = true; - buffer.push(x); - } - ')' => { - in_function = false; - buffer.push(x); - } - _ => buffer.push(x), - } - } - - if !buffer.is_empty() { - out.push(buffer); - } - - out - } - pub fn css(&self) -> String { let mut output = "