Compare commits

...

9 Commits

Author SHA1 Message Date
f8e2dad58a Set up key-value store in server main
All checks were successful
Push Workflows / rustfmt (push) Successful in 13s
Push Workflows / tailwind-build (push) Successful in 12s
Push Workflows / docs (push) Successful in 38s
Push Workflows / clippy (push) Successful in 41s
Push Workflows / test (push) Successful in 48s
Push Workflows / build (push) Successful in 1m45s
Push Workflows / nix-build (push) Successful in 5m21s
2026-06-27 15:28:52 -04:00
2b800c2df4 Add key-value store setup function 2026-06-27 15:28:39 -04:00
9e3d534190 Add config for key-value store connection 2026-06-27 15:26:07 -04:00
d74479851f Add format_uri function 2026-06-27 15:25:52 -04:00
4ecbf6da15 Add error type for key-value store 2026-06-27 15:17:22 -04:00
674b58e290 Use wildcard match for Error variants without special status codes 2026-06-27 15:17:02 -04:00
ef9f88e72c Add fred 2026-06-27 15:16:20 -04:00
97cf3f62ad Add User models 2026-06-27 13:24:12 -04:00
3677b6adfa Add cfg_if 2026-06-27 13:23:57 -04:00
9 changed files with 420 additions and 38 deletions

167
Cargo.lock generated
View File

@@ -32,6 +32,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arc-swap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "arraydeque" name = "arraydeque"
version = "0.5.1" version = "0.5.1"
@@ -262,6 +271,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bytes-utils"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
dependencies = [
"bytes",
"either",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.63" version = "1.2.63"
@@ -541,6 +560,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "cookie-factory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]] [[package]]
name = "cookie_store" name = "cookie_store"
version = "0.22.1" version = "0.22.1"
@@ -593,6 +618,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc16"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@@ -1562,6 +1593,15 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1583,6 +1623,43 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fred"
version = "10.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a7b2fd0f08b23315c13b6156f971aeedb6f75fb16a29ac1872d2eabccc1490e"
dependencies = [
"arc-swap",
"async-trait",
"bytes",
"bytes-utils",
"float-cmp",
"fred-macros",
"futures",
"log",
"parking_lot",
"rand 0.8.6",
"redis-protocol",
"semver",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tokio-util",
"url",
"urlencoding",
]
[[package]]
name = "fred-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1458c6e22d36d61507034d5afecc64f105c1d39712b7ac6ec3b352c423f715cc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.32" version = "0.3.32"
@@ -2000,7 +2077,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.4",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-layer", "tower-layer",
@@ -2314,6 +2391,7 @@ dependencies = [
name = "libretunes" name = "libretunes"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cfg-if",
"chrono", "chrono",
"config", "config",
"diesel", "diesel",
@@ -2321,6 +2399,7 @@ dependencies = [
"diesel_migrations", "diesel_migrations",
"dioxus", "dioxus",
"dotenvy", "dotenvy",
"fred",
"getrandom 0.4.3", "getrandom 0.4.3",
"lucide-dioxus", "lucide-dioxus",
"pbkdf2", "pbkdf2",
@@ -2535,6 +2614,12 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.1" version = "1.2.1"
@@ -2593,6 +2678,16 @@ dependencies = [
"jni-sys 0.3.1", "jni-sys 0.3.1",
] ]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.2" version = "0.2.2"
@@ -2957,7 +3052,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash 2.1.2", "rustc-hash 2.1.2",
"rustls", "rustls",
"socket2", "socket2 0.6.4",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@@ -2994,7 +3089,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2", "socket2 0.6.4",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -3020,13 +3115,24 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.4" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha 0.9.0",
"rand_core 0.9.5", "rand_core 0.9.5",
] ]
@@ -3041,6 +3147,16 @@ dependencies = [
"rand_core 0.10.1", "rand_core 0.10.1",
] ]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.9.0" version = "0.9.0"
@@ -3051,6 +3167,15 @@ dependencies = [
"rand_core 0.9.5", "rand_core 0.9.5",
] ]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.9.5" version = "0.9.5"
@@ -3072,6 +3197,20 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redis-protocol"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdba59219406899220fc4cdfd17a95191ba9c9afb719b5fa5a083d63109a9f1"
dependencies = [
"bytes",
"bytes-utils",
"cookie-factory",
"crc16",
"log",
"nom",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -3516,6 +3655,16 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.4" version = "0.6.4"
@@ -3765,7 +3914,7 @@ dependencies = [
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.4",
"tokio-macros", "tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -3801,7 +3950,7 @@ dependencies = [
"postgres-protocol", "postgres-protocol",
"postgres-types", "postgres-types",
"rand 0.10.1", "rand 0.10.1",
"socket2", "socket2 0.6.4",
"tokio", "tokio",
"tokio-util", "tokio-util",
"whoami", "whoami",
@@ -4193,6 +4342,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"

View File

@@ -9,6 +9,7 @@ edition = "2024"
build = "src/build.rs" build = "src/build.rs"
[dependencies] [dependencies]
cfg-if = "1.0.4"
chrono = { version = "0.4.45", features = ["serde"] } chrono = { version = "0.4.45", features = ["serde"] }
config = { version = "0.15.24", optional = true } config = { version = "0.15.24", optional = true }
diesel = { version = "2.3.10", optional = true, features = ["chrono"] } diesel = { version = "2.3.10", optional = true, features = ["chrono"] }
@@ -16,6 +17,7 @@ diesel-async = { version = "0.9.1", optional = true, features = ["postgres", "de
diesel_migrations = { version = "2.3.2", optional = true } diesel_migrations = { version = "2.3.2", optional = true }
dioxus = { version = "0.7.9", features = ["router", "fullstack"] } dioxus = { version = "0.7.9", features = ["router", "fullstack"] }
dotenvy = { version = "0.15.7", optional = true } dotenvy = { version = "0.15.7", optional = true }
fred = { version = "10.1.0", optional = true }
lucide-dioxus = { version = "3.11.0", features = ["notifications"] } lucide-dioxus = { version = "3.11.0", features = ["notifications"] }
pbkdf2 = { version = "0.13.0", optional = true, features = ["getrandom", "phc"] } pbkdf2 = { version = "0.13.0", optional = true, features = ["getrandom", "phc"] }
rand = "0.10.1" rand = "0.10.1"
@@ -34,6 +36,7 @@ server = [
"dep:diesel-async", "dep:diesel-async",
"dep:diesel_migrations", "dep:diesel_migrations",
"dep:dotenvy", "dep:dotenvy",
"dep:fred",
"dep:pbkdf2", "dep:pbkdf2",
"dep:tokio", "dep:tokio",
] ]

View File

@@ -1 +1 @@
pub mod user;

112
src/models/user.rs Normal file
View File

@@ -0,0 +1,112 @@
//! Various user types. Some types marked server-only to help prevent
//! leaking passwords to the frontend
/// Standard informational user type, contains no password information
#[derive(Clone, Debug)]
#[cfg_attr(feature = "server", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "server", diesel(table_name = crate::schema::users,
check_for_backend(diesel::pg::Pg)))]
pub struct User {
pub id: i32,
pub username: String,
pub created_at: chrono::DateTime<chrono::Local>,
}
/// Plaintext user credentials, used for login/signup form
pub struct UserCredentials {
pub username: String,
pub password: String,
}
cfg_if::cfg_if! {
if #[cfg(feature = "server")] {
use diesel::{
deserialize::{FromSql, FromSqlRow},
expression::AsExpression,
prelude::*,
serialize::ToSql,
sql_types,
};
use pbkdf2::{PasswordHasher, Pbkdf2};
/// Newtype for a `String`-represented hashed password
#[derive(Clone, Debug, AsExpression, FromSqlRow)]
#[diesel(sql_type = sql_types::Text)]
pub struct HashedPassword(String);
impl<DB> FromSql<diesel::sql_types::Text, DB> for HashedPassword
where
DB: diesel::backend::Backend,
String: FromSql<sql_types::Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
Ok(Self(String::from_sql(bytes)?))
}
}
impl<DB> ToSql<diesel::sql_types::Text, DB> for HashedPassword
where
DB: diesel::backend::Backend,
String: ToSql<sql_types::Text, DB>,
{
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, DB>,
) -> diesel::serialize::Result {
self.0.to_sql(out)
}
}
/// User as it appears in the database, with hashed password
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg))]
pub struct DbUser {
pub id: i32,
pub username: String,
pub hashed_password: HashedPassword,
pub created_at: chrono::DateTime<chrono::Local>,
}
impl From<DbUser> for User {
fn from(db_user: DbUser) -> Self {
User {
id: db_user.id,
username: db_user.username,
created_at: db_user.created_at,
}
}
}
/// User credentials with hashed password
#[derive(Clone, Debug, Insertable, Queryable, Selectable)]
#[diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg))]
pub struct HashedUserCredentials {
username: String,
hashed_password: HashedPassword,
}
impl From<DbUser> for HashedUserCredentials {
fn from(db_user: DbUser) -> Self {
HashedUserCredentials {
username: db_user.username,
hashed_password: db_user.hashed_password,
}
}
}
impl UserCredentials {
/// Attempt to convert into `HashedUserCredentials` by hashing the password. Yields a PBKDF2
/// error on failure.
pub fn try_hash(self) -> Result<HashedUserCredentials, pbkdf2::password_hash::Error> {
let hashed_password = Pbkdf2::default().hash_password(self.password.as_bytes())?;
Ok(HashedUserCredentials {
username: self.username,
hashed_password: HashedPassword(hashed_password.to_string()),
})
}
}
}
}

View File

@@ -1,5 +1,39 @@
use serde::Deserialize; use serde::Deserialize;
/// Build a connection URI from parts
fn format_uri(
scheme: &str,
username: &Option<String>,
password: &Option<String>,
host: &str,
port: &Option<u16>,
path: &Option<String>,
) -> String {
let mut url = format!("{scheme}://");
if let Some(username) = username {
url.push_str(username);
if let Some(password) = password {
url.push_str(&format!(":{password}"));
}
url.push('@');
}
url.push_str(host);
if let Some(port) = port {
url.push_str(&format!(":{port}"));
}
if let Some(path) = path {
url.push_str(&format!("/{path}"));
}
url
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig { pub struct DatabaseConfig {
#[serde(flatten)] #[serde(flatten)]
@@ -39,39 +73,69 @@ impl DatabaseConnectionConfig {
database, database,
username, username,
password, password,
} => { } => format_uri("postgres", username, password, host, port, database),
let mut url = "postgres://".to_string();
if let Some(username) = username {
url.push_str(username);
if let Some(password) = password {
url.push_str(&format!(":{password}"));
}
url.push('@');
}
url.push_str(host);
if let Some(port) = port {
url.push_str(&format!(":{port}"));
}
if let Some(database) = database {
url.push_str(&format!("/{database}"));
}
url
} }
} }
} }
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum KeyValStoreConnectionConfig {
FromUrl {
url: String,
},
FromParts {
scheme: Option<String>,
host: String,
port: Option<u16>,
database: Option<String>,
username: Option<String>,
password: Option<String>,
},
}
impl KeyValStoreConnectionConfig {
/// Convert this configuration into the Redis connection URI
pub fn as_uri(&self) -> String {
match self {
Self::FromUrl { url } => url.clone(),
Self::FromParts {
scheme,
host,
port,
database,
username,
password,
} => format_uri(
scheme.as_deref().unwrap_or("redis"),
username,
password,
host,
port,
database,
),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct KeyValStoreConfig {
#[serde(flatten)]
connection: KeyValStoreConnectionConfig,
}
impl KeyValStoreConfig {
/// Get the configured database connection URI
pub fn connection_uri(&self) -> String {
self.connection.as_uri()
}
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
/// Top-level application configuration /// Top-level application configuration
pub struct Config { pub struct Config {
pub database: DatabaseConfig, pub database: DatabaseConfig,
pub key_val_store: KeyValStoreConfig,
} }
/// Parse configuration from the expected files and environment variables /// Parse configuration from the expected files and environment variables

View File

@@ -0,0 +1,29 @@
use fred::prelude::*;
use crate::util::error::{Contextualize, Error, ErrorType};
const KEY_VAL_POOL_SIZE: usize = 4;
pub type KeyValPool = Pool;
pub async fn setup(connection_uri: &str) -> Result<KeyValPool, Error> {
let config = Config::from_url(connection_uri)
.map_err(|e| ErrorType::KeyValStore(e.to_string()))
.err_context("Error creating key-value store config")?;
let pool = Builder::from_config(config)
.build_pool(KEY_VAL_POOL_SIZE)
// At time of writing the only error that could occur here is if config is not provided.
// Since we're building a pool `from_config`, this shouldn't be possible
.map_err(|e| ErrorType::KeyValStore(e.to_string()))
.err_context("Error creating pool for key-value store")?;
tracing::debug!("Establishing connection to key-value store...");
pool.init()
.await
.map_err(|e| ErrorType::KeyValStore(e.to_string()))
.err_context("Error connecting to key-value store")?;
Ok(pool)
}

View File

@@ -1,7 +1,7 @@
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::App; use crate::App;
use crate::server::{config, database}; use crate::server::{config, database, key_val_store};
use crate::util::error::{Contextualize, Error, Result}; use crate::util::error::{Contextualize, Error, Result};
pub fn main() -> Result<()> { pub fn main() -> Result<()> {
@@ -22,10 +22,16 @@ pub fn main() -> Result<()> {
.map_err(|e| Error::message_here(e.to_string())) .map_err(|e| Error::message_here(e.to_string()))
.err_context("Failed to create tokio runtime for server setup")?; .err_context("Failed to create tokio runtime for server setup")?;
let _db_pool = setup_rt.block_on(async { let (_db_pool, _key_val_pool) = setup_rt.block_on(async {
database::setup(config.database.connection_uri()) let db_pool = database::setup(config.database.connection_uri())
.await .await
.err_context("Failed database setup") .err_context("Failed database setup")?;
let key_val_pool = key_val_store::setup(&config.key_val_store.connection_uri())
.await
.err_context("Failed key-value store setup")?;
Ok::<_, Error>((db_pool, key_val_pool))
})?; })?;
tracing::info!("Setup complete, launching web server..."); tracing::info!("Setup complete, launching web server...");

View File

@@ -1,5 +1,6 @@
pub mod config; pub mod config;
pub mod database; pub mod database;
pub mod key_val_store;
pub mod main; pub mod main;
pub use main::main; pub use main::main;

View File

@@ -242,9 +242,8 @@ impl dioxus_fullstack::AsStatusCode for Error {
ErrorType::Database(msg) if *msg == (diesel::result::Error::NotFound).to_string() => { ErrorType::Database(msg) if *msg == (diesel::result::Error::NotFound).to_string() => {
StatusCode::NOT_FOUND StatusCode::NOT_FOUND
} }
ErrorType::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::Error(_) => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::ServerFnError(e) => e.as_status_code(), ErrorType::ServerFnError(e) => e.as_status_code(),
_ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
} }
@@ -293,6 +292,11 @@ pub enum ErrorType {
#[error("Server function error: {0}")] #[error("Server function error: {0}")]
ServerFnError(ServerFnError), ServerFnError(ServerFnError),
// Using string to represent Fred errors, because Fred's Error type is not `Serialize`,
// and Fred is only available on the server
#[error("Key-value store error: {0}")]
KeyValStore(String),
} }
impl From<ErrorType> for Error { impl From<ErrorType> for Error {
@@ -309,3 +313,11 @@ impl From<diesel::result::Error> for Error {
Error::new_here(ErrorType::Database(format!("{err}"))) Error::new_here(ErrorType::Database(format!("{err}")))
} }
} }
#[cfg(feature = "server")]
impl From<fred::error::Error> for Error {
#[track_caller]
fn from(err: fred::error::Error) -> Self {
Error::new_here(ErrorType::KeyValStore(format!("{err}")))
}
}