From b530de8f52429b10d7cee257df4e9fe131c6b1df Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Sat, 3 May 2025 02:20:37 +0200 Subject: [PATCH 01/10] fix: username regex --- src/api/v1/auth/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index ff74c6b..64dd5f6 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -19,8 +19,7 @@ static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() }); -// FIXME: This regex doesnt seem to be working -static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[a-zA-Z0-9.-_]").unwrap()); +static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[\w.-]+$").unwrap()); // Password is expected to be hashed using SHA3-384 static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); From e29940d080cf1ea9b97558cd0fc60c7ae28ca4bc Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Sat, 3 May 2025 03:04:07 +0200 Subject: [PATCH 02/10] feat: only allow lowercase usernames --- src/api/v1/auth/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 64dd5f6..a9ee803 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -19,7 +19,7 @@ static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() }); -static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[\w.-]+$").unwrap()); +static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z_.-]+$").unwrap()); // Password is expected to be hashed using SHA3-384 static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); From ab5c85c4f565dfd4edf0f2339c0bef7f4d3acbba Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Sun, 4 May 2025 23:25:48 +0200 Subject: [PATCH 03/10] fix: add numbers to username regex --- src/api/v1/auth/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index a9ee803..69f3fe3 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -19,7 +19,7 @@ static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() }); -static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z_.-]+$").unwrap()); +static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); // Password is expected to be hashed using SHA3-384 static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); From 77245e98c51e67a24e94400219cf9b39cfcdae54 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 4 May 2025 23:50:38 +0200 Subject: [PATCH 04/10] refactor: combine crypto.rs with utils.rs --- src/api/v1/auth/login.rs | 2 +- src/api/v1/auth/refresh.rs | 2 +- src/api/v1/auth/register.rs | 2 +- src/crypto.rs | 14 -------------- src/main.rs | 2 +- src/utils.rs | 17 ++++++++++++++++- 6 files changed, 20 insertions(+), 19 deletions(-) delete mode 100644 src/crypto.rs diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 0ea3d83..bc6af8c 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -7,7 +7,7 @@ use log::error; use serde::Deserialize; use crate::{ - api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, crypto::{generate_access_token, generate_refresh_token}, utils::refresh_token_cookie, Data + api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data }; use super::Response; diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 8c3e7d0..008420b 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -3,7 +3,7 @@ use log::error; use std::time::{SystemTime, UNIX_EPOCH}; use crate::{ - crypto::{generate_access_token, generate_refresh_token}, utils::refresh_token_cookie, Data + utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data }; use super::Response; diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 7bbbbae..a56dd0e 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use super::Response; use crate::{ - api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, crypto::{generate_access_token, generate_refresh_token}, utils::refresh_token_cookie, Data + api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data }; #[derive(Deserialize)] diff --git a/src/crypto.rs b/src/crypto.rs deleted file mode 100644 index c4d96c8..0000000 --- a/src/crypto.rs +++ /dev/null @@ -1,14 +0,0 @@ -use getrandom::fill; -use hex::encode; - -pub fn generate_access_token() -> Result { - let mut buf = [0u8; 16]; - fill(&mut buf)?; - Ok(encode(buf)) -} - -pub fn generate_refresh_token() -> Result { - let mut buf = [0u8; 32]; - fill(&mut buf)?; - Ok(encode(buf)) -} diff --git a/src/main.rs b/src/main.rs index e967021..36fa6ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; mod api; -pub mod crypto; + pub mod utils; type Error = Box; diff --git a/src/utils.rs b/src/utils.rs index b432d19..6571fab 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,6 @@ use actix_web::{cookie::{time::Duration, Cookie, SameSite}, http::header::HeaderMap, HttpResponse}; +use getrandom::fill; +use hex::encode; pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, HttpResponse> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); @@ -30,4 +32,17 @@ pub fn refresh_token_cookie(refresh_token: String) -> Cookie<'static> { .path("/api") .max_age(Duration::days(30)) .finish() -} +} + +pub fn generate_access_token() -> Result { + let mut buf = [0u8; 16]; + fill(&mut buf)?; + Ok(encode(buf)) +} + +pub fn generate_refresh_token() -> Result { + let mut buf = [0u8; 32]; + fill(&mut buf)?; + Ok(encode(buf)) +} + From c0f2948b760029314b87b94fc920852365edadac Mon Sep 17 00:00:00 2001 From: SauceyRed Date: Tue, 6 May 2025 00:41:23 +0200 Subject: [PATCH 05/10] feat: implement cors --- Cargo.toml | 1 + src/main.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e34d9b6..aca7977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ lto = true codegen-units = 1 [dependencies] +actix-cors = "0.7.1" actix-web = "4.10" argon2 = { version = "0.5.3", features = ["std"] } clap = { version = "4.5.37", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs index 36fa6ba..48b1c4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use actix_cors::Cors; use actix_web::{App, HttpServer, web}; use argon2::Argon2; use clap::Parser; @@ -86,9 +87,38 @@ async fn main() -> Result<(), Error> { start_time: SystemTime::now(), }; + HttpServer::new(move || { + // Set CORS headers + let cors = Cors::default() + /* + Set Allowed-Control-Allow-Origin header to whatever + the request's Origin header is. Must be done like this + rather than setting it to "*" due to CORS not allowing + sending of credentials (cookies) with wildcard origin. + */ + .allowed_origin_fn(|_origin, _req_head| { + true + }) + /* + Allows any request method in CORS preflight requests. + This will be restricted to only ones actually in use later. + */ + .allow_any_method() + /* + Allows any header(s) in request in CORS preflight requests. + This wll be restricted to only ones actually in use later. + */ + .allow_any_header() + /* + Allows browser to include cookies in requests. + This is needed for receiving the secure HttpOnly refresh_token cookie. + */ + .supports_credentials(); + App::new() .app_data(web::Data::new(data.clone())) + .wrap(cors) .service(api::web()) }) .bind((web.url, web.port))? From 7ecc8c4270eb4a477f18630b1542e062e2026378 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 20:32:32 +0200 Subject: [PATCH 06/10] feat: add redis caching --- Cargo.toml | 1 + src/config.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 7 ++++++- src/utils.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index aca7977..4e2f58d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple_logger = "5.0.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] } +redis = { version = "0.30", features= ["tokio-comp"] } toml = "0.8" url = { version = "2.5", features = ["serde"] } uuid = { version = "1.16", features = ["serde", "v7"] } diff --git a/src/config.rs b/src/config.rs index a2a6192..65a5965 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use tokio::fs::read_to_string; #[derive(Debug, Deserialize)] pub struct ConfigBuilder { database: Database, + cache_database: CacheDatabase, web: Option, } @@ -19,6 +20,15 @@ pub struct Database { port: u16, } +#[derive(Debug, Deserialize, Clone)] +pub struct CacheDatabase { + username: Option, + password: Option, + host: String, + database: Option, + port: u16, +} + #[derive(Debug, Deserialize)] struct WebBuilder { url: Option, @@ -51,6 +61,7 @@ impl ConfigBuilder { Config { database: self.database, + cache_database: self.cache_database, web, } } @@ -59,6 +70,7 @@ impl ConfigBuilder { #[derive(Debug, Clone)] pub struct Config { pub database: Database, + pub cache_database: CacheDatabase, pub web: Web, } @@ -78,3 +90,33 @@ impl Database { .port(self.port) } } + +impl CacheDatabase { + pub fn url(&self) -> String { + let mut url = String::from("redis://"); + + if let Some(username) = &self.username { + url += username; + } + + if let Some(password) = &self.password { + url += ":"; + url += password; + } + + if self.username.is_some() || self.password.is_some() { + url += "@"; + } + + url += &self.host; + url += ":"; + url += &self.port.to_string(); + + if let Some(database) = &self.database { + url += "/"; + url += database; + } + + url + } +} diff --git a/src/main.rs b/src/main.rs index 48b1c4a..c93e760 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ use actix_cors::Cors; use actix_web::{App, HttpServer, web}; use argon2::Argon2; use clap::Parser; +use redis::aio::MultiplexedConnection; use simple_logger::SimpleLogger; use sqlx::{PgPool, Pool, Postgres}; -use std::time::SystemTime; +use std::{cell::Cell, time::SystemTime}; mod config; use config::{Config, ConfigBuilder}; mod api; @@ -23,6 +24,7 @@ struct Args { #[derive(Clone)] struct Data { pub pool: Pool, + pub cache_pool: redis::Client, pub _config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, @@ -44,6 +46,8 @@ async fn main() -> Result<(), Error> { let pool = PgPool::connect_with(config.database.connect_options()).await?; + let cache_pool = redis::Client::open(config.cache_database.url())?; + /* TODO: Figure out if a table should be used here and if not then what. Also figure out if these should be different types from what they currently are and if we should add more "constraints" @@ -81,6 +85,7 @@ async fn main() -> Result<(), Error> { let data = Data { pool, + cache_pool, _config: config, // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) argon2: Argon2::default(), diff --git a/src/utils.rs b/src/utils.rs index 6571fab..5d6b51c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,11 @@ use actix_web::{cookie::{time::Duration, Cookie, SameSite}, http::header::HeaderMap, HttpResponse}; use getrandom::fill; use hex::encode; +use redis::{AsyncCommands, RedisError}; +use serde::Serialize; +use serde_json::json; + +use crate::Data; pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, HttpResponse> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); @@ -46,3 +51,25 @@ pub fn generate_refresh_token() -> Result { Ok(encode(buf)) } +impl Data { + pub async fn set_cache_key(&self, key: String, value: impl Serialize, expire: u32) -> Result<(), RedisError> { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + + let key_encoded = encode(key); + + let value_json = json!(value).to_string(); + + redis::cmd("SET",).arg(&[key_encoded.clone(), value_json]).exec_async(&mut conn).await?; + + redis::cmd("EXPIRE").arg(&[key_encoded, expire.to_string()]).exec_async(&mut conn).await + } + + pub async fn get_cache_key(&self, key: String) -> Result { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + + let key_encoded = encode(key); + + redis::cmd("GET").arg(key_encoded).query_async(&mut conn).await + } +} + From 529ccd1b518fb73c5cc619e873fa39f11a905e02 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 20:33:23 +0200 Subject: [PATCH 07/10] feat: use caching on user lookup this needs to be deleted/expired on user update, we'll implement this when we get ways to "update" things like channels, servers and users --- src/api/v1/users/uuid.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index f4c1f13..577af0e 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,11 +1,12 @@ use actix_web::{Error, HttpRequest, HttpResponse, get, web}; use log::error; use serde::Serialize; +use tokio::sync::Mutex; use uuid::Uuid; use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; -#[derive(Serialize)] +#[derive(Serialize, Clone)] struct Response { uuid: String, username: String, @@ -34,6 +35,12 @@ pub async fn res( return Ok(error); } + let cache_result = data.get_cache_key(uuid.to_string()).await; + + if let Ok(cache_hit) = cache_result { + return Ok(HttpResponse::Ok().json(cache_hit)) + } + let row = sqlx::query_as(&format!( "SELECT username, display_name FROM users WHERE uuid = '{}'", uuid @@ -48,9 +55,18 @@ pub async fn res( let (username, display_name): (String, Option) = row.unwrap(); - Ok(HttpResponse::Ok().json(Response { + let user = Response { uuid: uuid.to_string(), username, display_name: display_name.unwrap_or_default(), - })) + }; + + let cache_result = data.set_cache_key(uuid.to_string(), user.clone(), 1800).await; + + if let Err(error) = cache_result { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(user)) } From 3e64a49338626bb782b33567fc3d3d30dfbfede7 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 20:57:01 +0200 Subject: [PATCH 08/10] chore: add valkey configuration to docker --- Dockerfile | 8 +++++++- compose.dev.yml | 5 +++++ compose.yml | 5 +++++ entrypoint.sh | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7867f8b..d9a0389 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,12 @@ RUN useradd --create-home --home-dir /gorb gorb USER gorb -ENV DATABASE_USERNAME="gorb" DATABASE_PASSWORD="gorb" DATABASE="gorb" DATABASE_HOST="localhost" DATABASE_PORT="5432" +ENV DATABASE_USERNAME="gorb" \ +DATABASE_PASSWORD="gorb" \ +DATABASE="gorb" \ +DATABASE_HOST="database" \ +DATABASE_PORT="5432" \ +CACHE_DB_HOST="valkey" \ +CACHE_DB_PORT="6379" ENTRYPOINT ["/usr/bin/entrypoint.sh"] diff --git a/compose.dev.yml b/compose.dev.yml index 02f46a3..d064beb 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -34,3 +34,8 @@ services: - POSTGRES_USER=gorb - POSTGRES_PASSWORD=gorb - POSTGRES_DB=gorb + valkey: + image: valkey/valkey + restart: always + networks: + - gorb diff --git a/compose.yml b/compose.yml index 4544dea..84e6695 100644 --- a/compose.yml +++ b/compose.yml @@ -32,3 +32,8 @@ services: - POSTGRES_USER=gorb - POSTGRES_PASSWORD=gorb - POSTGRES_DB=gorb + valkey: + image: valkey/valkey + restart: always + networks: + - gorb diff --git a/entrypoint.sh b/entrypoint.sh index 63bfa84..a212f8e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -16,6 +16,10 @@ password = "${DATABASE_PASSWORD}" database = "${DATABASE}" host = "${DATABASE_HOST}" port = ${DATABASE_PORT} + +[cache_database] +host = "${CACHE_DB_HOST}" +port = ${CACHE_DB_PORT} EOF fi From 9e56eec0217e0b92817c909575ce2cd0ac1ae44b Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 21:22:38 +0200 Subject: [PATCH 09/10] fix: remove unused imports --- src/api/v1/users/uuid.rs | 1 - src/main.rs | 3 +-- src/utils.rs | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 577af0e..04c05bd 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,7 +1,6 @@ use actix_web::{Error, HttpRequest, HttpResponse, get, web}; use log::error; use serde::Serialize; -use tokio::sync::Mutex; use uuid::Uuid; use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; diff --git a/src/main.rs b/src/main.rs index c93e760..7f21e2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,9 @@ use actix_cors::Cors; use actix_web::{App, HttpServer, web}; use argon2::Argon2; use clap::Parser; -use redis::aio::MultiplexedConnection; use simple_logger::SimpleLogger; use sqlx::{PgPool, Pool, Postgres}; -use std::{cell::Cell, time::SystemTime}; +use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; mod api; diff --git a/src/utils.rs b/src/utils.rs index 5d6b51c..2d63024 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,7 @@ use actix_web::{cookie::{time::Duration, Cookie, SameSite}, http::header::HeaderMap, HttpResponse}; use getrandom::fill; use hex::encode; -use redis::{AsyncCommands, RedisError}; +use redis::RedisError; use serde::Serialize; use serde_json::json; @@ -57,7 +57,7 @@ impl Data { let key_encoded = encode(key); - let value_json = json!(value).to_string(); + let value_json = json!(value).as_str().unwrap().to_string(); redis::cmd("SET",).arg(&[key_encoded.clone(), value_json]).exec_async(&mut conn).await?; From 3e65cffe39e64e8ad91d759dbf2ee332116e9f91 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 22:21:59 +0200 Subject: [PATCH 10/10] fix: fix user uuid cache hits --- src/api/v1/users/uuid.rs | 2 +- src/utils.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 04c05bd..5e4db39 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -37,7 +37,7 @@ pub async fn res( let cache_result = data.get_cache_key(uuid.to_string()).await; if let Ok(cache_hit) = cache_result { - return Ok(HttpResponse::Ok().json(cache_hit)) + return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit)) } let row = sqlx::query_as(&format!( diff --git a/src/utils.rs b/src/utils.rs index 2d63024..15e5e2e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,7 +3,6 @@ use getrandom::fill; use hex::encode; use redis::RedisError; use serde::Serialize; -use serde_json::json; use crate::Data; @@ -57,7 +56,7 @@ impl Data { let key_encoded = encode(key); - let value_json = json!(value).as_str().unwrap().to_string(); + let value_json = serde_json::to_string(&value).unwrap(); redis::cmd("SET",).arg(&[key_encoded.clone(), value_json]).exec_async(&mut conn).await?;