This commit is contained in:
Greg Burri 2025-05-07 20:12:49 +02:00
parent 198bff6e4a
commit 6f014ef238
19 changed files with 118 additions and 81 deletions

View file

@ -355,11 +355,11 @@ fn url_rewriting(mut req: Request) -> Request {
} }
/// The language associated to the current HTTP request is defined in the current order: /// The language associated to the current HTTP request is defined in the current order:
/// - Extraction from the url: like in `/fr/recipe/view/42` /// 1. Extraction from the url: like in `/fr/recipe/view/42`
/// - Get from the user database record. /// 2. Get from the user database record.
/// - Get from the cookie. /// 3. Get from the cookie.
/// - Get from the HTTP header `accept-language`. /// 4. Get from the HTTP header `accept-language`.
/// - Set as `translation::DEFAULT_LANGUAGE_CODE`. /// 5. Set as [translation::DEFAULT_LANGUAGE_CODE].
async fn context( async fn context(
ConnectInfo(addr): ConnectInfo<SocketAddr>, ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(connection): State<db::Connection>, State(connection): State<db::Connection>,

View file

@ -17,6 +17,10 @@ pub struct Config {
#[serde(default = "port_default")] #[serde(default = "port_default")]
pub port: u16, pub port: u16,
/// The email address used when sending validation email.
#[serde(default = "email_address_default")]
pub email_address: String,
#[serde(default = "smtp_relay_address_default")] #[serde(default = "smtp_relay_address_default")]
pub smtp_relay_address: String, pub smtp_relay_address: String,
@ -44,11 +48,14 @@ pub struct Config {
fn port_default() -> u16 { fn port_default() -> u16 {
8082 8082
} }
fn smtp_relay_address_default() -> String { fn smtp_relay_address_default() -> String {
"mail.something.com".to_string() "mail.something.com".to_string()
} }
fn email_address_default() -> String {
"".to_string()
}
fn smtp_login_default() -> String { fn smtp_login_default() -> String {
"login".to_string() "login".to_string()
} }

View file

@ -38,9 +38,6 @@ pub const COOKIE_LANG_NAME: &str = "lang";
/// (cookie authentication, password reset, validation token). /// (cookie authentication, password reset, validation token).
pub const TOKEN_SIZE: usize = 32; pub const TOKEN_SIZE: usize = 32;
// TODO: Move it in conf.ron.
pub const EMAIL_ADDRESS: &str = "recipes@gburri.org";
/// When sending a validation email, /// When sending a validation email,
/// the server has this duration to wait for a response from the SMTP server. /// the server has this duration to wait for a response from the SMTP server.
pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60); pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60);

View file

@ -4,7 +4,6 @@ use lettre::{
AsyncTransport, Message, Tokio1Executor, AsyncTransport, Message, Tokio1Executor,
transport::smtp::{AsyncSmtpTransport, authentication::Credentials}, transport::smtp::{AsyncSmtpTransport, authentication::Credentials},
}; };
use tracing::error;
use crate::consts; use crate::consts;
@ -22,7 +21,13 @@ pub enum Error {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait EmailServiceTrait: Send + Sync { pub trait EmailServiceTrait: Send + Sync {
async fn send_email(&self, email: &str, title: &str, message: &str) -> Result<(), Error>; async fn send_email(
&self,
email_sender: &str,
email_receiver: &str,
title: &str,
message: &str,
) -> Result<(), Error>;
} }
pub struct EmailService { pub struct EmailService {
@ -49,11 +54,17 @@ impl EmailService {
impl EmailServiceTrait for EmailService { impl EmailServiceTrait for EmailService {
/// A function to send an email using the given SMTP address. /// A function to send an email using the given SMTP address.
/// It may timeout if the SMTP server is not reachable, see [consts::SEND_EMAIL_TIMEOUT]. /// It may timeout if the SMTP server is not reachable, see [consts::SEND_EMAIL_TIMEOUT].
async fn send_email(&self, email: &str, title: &str, message: &str) -> Result<(), Error> { async fn send_email(
&self,
email_sender: &str,
email_receiver: &str,
title: &str,
message: &str,
) -> Result<(), Error> {
let email = Message::builder() let email = Message::builder()
.message_id(None) .message_id(None)
.from(consts::EMAIL_ADDRESS.parse()?) .from(email_sender.parse()?)
.to(email.parse()?) .to(email_receiver.parse()?)
.subject(title) .subject(title)
.body(message.to_string())?; .body(message.to_string())?;

View file

@ -5,8 +5,10 @@ use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng}, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
}; };
const MAX_LENGTH_PASSWORD: usize = 255;
fn get_argon2<'k>() -> Argon2<'k> { fn get_argon2<'k>() -> Argon2<'k> {
// Note: It's not neccessary to have only one Argon2 object, creating a new one // Note: It's not necessary to have only one Argon2 object, creating a new one
// when we need it is lightweight. // when we need it is lightweight.
Argon2::new( Argon2::new(
argon2::Algorithm::Argon2id, argon2::Algorithm::Argon2id,
@ -22,6 +24,14 @@ fn get_argon2<'k>() -> Argon2<'k> {
} }
pub fn hash(password: &str) -> Result<String, Box<dyn std::error::Error>> { pub fn hash(password: &str) -> Result<String, Box<dyn std::error::Error>> {
if password.len() > MAX_LENGTH_PASSWORD {
return Err(format!("Password max length is {}", MAX_LENGTH_PASSWORD).into());
}
if password.is_empty() {
return Err("Password can't be empty".into());
}
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let argon2 = get_argon2(); let argon2 = get_argon2();
argon2 argon2

View file

@ -44,7 +44,14 @@ impl Log {
P: AsRef<Path>, P: AsRef<Path>,
{ {
if !directory.as_ref().exists() { if !directory.as_ref().exists() {
fs::DirBuilder::new().create(&directory).unwrap(); fs::DirBuilder::new()
.create(&directory)
.unwrap_or_else(|_| {
panic!(
"Unable to create directory: {}",
directory.as_ref().to_string_lossy()
)
});
} }
let file_appender = RollingFileAppender::builder() let file_appender = RollingFileAppender::builder()
@ -78,7 +85,7 @@ impl Log {
} }
} }
pub fn new_stdout_only() -> Self { pub fn new_to_stdout_only() -> Self {
let layer_stdout = tracing_subscriber::fmt::layer() let layer_stdout = tracing_subscriber::fmt::layer()
.with_writer(std::io::stdout.with_max_level(TRACING_LEVEL)) .with_writer(std::io::stdout.with_max_level(TRACING_LEVEL))
.with_thread_ids(TRACING_DISPLAY_THREAD) .with_thread_ids(TRACING_DISPLAY_THREAD)

View file

@ -1,4 +1,5 @@
//! An Axum extractor for HTTP body containing RON data (Rusty Object Notation). //! An Axum extractor for HTTP body containing RON data (Rusty Object Notation).
use axum::{ use axum::{
body::Bytes, body::Bytes,
extract::{FromRequest, Request}, extract::{FromRequest, Request},

View file

@ -18,13 +18,15 @@ pub struct RonError {
impl axum::response::IntoResponse for RonError { impl axum::response::IntoResponse for RonError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let ron_as_str = ron_api::to_string(&self); match ron_api::to_string(&self) {
( Ok(ron_as_str) => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)], [(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
ron_as_str, ron_as_str,
) )
.into_response() .into_response(),
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
} }
} }
@ -44,7 +46,7 @@ pub fn ron_error(status: StatusCode, message: &str) -> impl IntoResponse {
) )
} }
pub fn ron_error_not_autorized() -> ErrorResponse { pub fn ron_error_not_authorized() -> ErrorResponse {
ErrorResponse::from(ron_error( ErrorResponse::from(ron_error(
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
consts::NOT_AUTHORIZED_MESSAGE, consts::NOT_AUTHORIZED_MESSAGE,
@ -58,16 +60,19 @@ where
ron_response(StatusCode::OK, ron) ron_response(StatusCode::OK, ron)
} }
pub fn ron_response<T>(status: StatusCode, ron: T) -> impl IntoResponse pub fn ron_response<T>(status: StatusCode, ron: T) -> Response
where where
T: Serialize, T: Serialize,
{ {
let ron_as_str = ron_api::to_string(&ron); match ron_api::to_string(&ron) {
( Ok(ron_as_str) => (
status, status,
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)], [(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
ron_as_str, ron_as_str,
) )
.into_response(),
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
} }
pub fn parse_body<T>(body: Bytes) -> Result<T, RonError> pub fn parse_body<T>(body: Bytes) -> Result<T, RonError>

View file

@ -5,7 +5,6 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use serde::Deserialize; use serde::Deserialize;
// use tracing::{event, Level};
use crate::{ use crate::{
app::{Context, Result}, app::{Context, Result},

View file

@ -1,6 +1,6 @@
use axum::response::Result; use axum::response::Result;
use crate::{data::db, data::model, ron_utils::ron_error_not_autorized}; use crate::{data::db, data::model, ron_utils::ron_error_not_authorized};
pub async fn check_user_rights_recipe( pub async fn check_user_rights_recipe(
connection: &db::Connection, connection: &db::Connection,
@ -9,7 +9,7 @@ pub async fn check_user_rights_recipe(
) -> Result<()> { ) -> Result<()> {
match user { match user {
Some(user) if connection.can_edit_recipe(user.id, recipe_id).await? => Ok(()), Some(user) if connection.can_edit_recipe(user.id, recipe_id).await? => Ok(()),
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -20,7 +20,7 @@ pub async fn check_user_rights_recipe_group(
) -> Result<()> { ) -> Result<()> {
match user { match user {
Some(user) if connection.can_edit_recipe_group(user.id, group_id).await? => Ok(()), Some(user) if connection.can_edit_recipe_group(user.id, group_id).await? => Ok(()),
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -37,7 +37,7 @@ pub async fn check_user_rights_recipe_groups(
{ {
Ok(()) Ok(())
} }
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -48,7 +48,7 @@ pub async fn check_user_rights_recipe_step(
) -> Result<()> { ) -> Result<()> {
match user { match user {
Some(user) if connection.can_edit_recipe_step(user.id, step_id).await? => Ok(()), Some(user) if connection.can_edit_recipe_step(user.id, step_id).await? => Ok(()),
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -65,7 +65,7 @@ pub async fn check_user_rights_recipe_steps(
{ {
Ok(()) Ok(())
} }
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -82,7 +82,7 @@ pub async fn check_user_rights_recipe_ingredient(
{ {
Ok(()) Ok(())
} }
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -99,7 +99,7 @@ pub async fn check_user_rights_recipe_ingredients(
{ {
Ok(()) Ok(())
} }
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }
@ -116,6 +116,6 @@ pub async fn check_user_rights_shopping_list_entry(
{ {
Ok(()) Ok(())
} }
_ => Err(ron_error_not_autorized()), _ => Err(ron_error_not_authorized()),
} }
} }

View file

@ -20,6 +20,7 @@ use tracing::{error, warn};
use crate::{ use crate::{
app::{AppState, Context, Result}, app::{AppState, Context, Result},
config::Config,
consts, consts,
data::db, data::db,
email, email,
@ -88,6 +89,7 @@ enum SignUpError {
#[debug_handler(state = AppState)] #[debug_handler(state = AppState)]
pub async fn sign_up_post( pub async fn sign_up_post(
Host(host): Host, Host(host): Host,
State(config): State<Config>,
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
State(email_service): State<Arc<dyn email::EmailServiceTrait>>, State(email_service): State<Arc<dyn email::EmailServiceTrait>>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
@ -171,6 +173,7 @@ pub async fn sign_up_post(
let email = form_data.email.clone(); let email = form_data.email.clone();
match email_service match email_service
.send_email( .send_email(
&config.email_address,
&email, &email,
context.tr.t(Sentence::SignUpEmailTitle), context.tr.t(Sentence::SignUpEmailTitle),
&context.tr.tp( &context.tr.tp(
@ -385,7 +388,6 @@ pub async fn sign_in_post(
#[debug_handler] #[debug_handler]
pub async fn sign_out( pub async fn sign_out(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
req: Request<Body>, req: Request<Body>,
) -> Result<(CookieJar, Redirect)> { ) -> Result<(CookieJar, Redirect)> {
let mut jar = CookieJar::from_headers(req.headers()); let mut jar = CookieJar::from_headers(req.headers());
@ -450,6 +452,7 @@ enum AskResetPasswordError {
#[debug_handler(state = AppState)] #[debug_handler(state = AppState)]
pub async fn ask_reset_password_post( pub async fn ask_reset_password_post(
Host(host): Host, Host(host): Host,
State(config): State<Config>,
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
State(email_service): State<Arc<dyn email::EmailServiceTrait>>, State(email_service): State<Arc<dyn email::EmailServiceTrait>>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
@ -522,6 +525,7 @@ pub async fn ask_reset_password_post(
let url = utils::get_url_from_host(&host); let url = utils::get_url_from_host(&host);
match email_service match email_service
.send_email( .send_email(
&config.email_address,
&form_data.email, &form_data.email,
context.tr.t(Sentence::AskResetEmailTitle), context.tr.t(Sentence::AskResetEmailTitle),
&context.tr.tp( &context.tr.tp(
@ -755,6 +759,7 @@ enum ProfileUpdateError {
#[debug_handler(state = AppState)] #[debug_handler(state = AppState)]
pub async fn edit_user_post( pub async fn edit_user_post(
Host(host): Host, Host(host): Host,
State(config): State<Config>,
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
State(email_service): State<Arc<dyn email::EmailServiceTrait>>, State(email_service): State<Arc<dyn email::EmailServiceTrait>>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
@ -859,6 +864,7 @@ pub async fn edit_user_post(
let email = form_data.email.clone(); let email = form_data.email.clone();
match email_service match email_service
.send_email( .send_email(
&config.email_address,
&email, &email,
context.tr.t(Sentence::ProfileFollowEmailTitle), context.tr.t(Sentence::ProfileFollowEmailTitle),
&context.tr.tp( &context.tr.tp(

View file

@ -19,12 +19,15 @@ pub struct Tr {
} }
impl Tr { impl Tr {
/// Create a new translation object.
/// See [available_codes].
pub fn new(code: &str) -> Self { pub fn new(code: &str) -> Self {
Self { Self {
lang: get_language_translation(code), lang: get_language_translation(code),
} }
} }
/// Translate the given sentence according to the current language.
pub fn t<T>(&self, sentence: T) -> &'static str pub fn t<T>(&self, sentence: T) -> &'static str
where where
T: Borrow<Sentence>, T: Borrow<Sentence>,
@ -32,11 +35,13 @@ impl Tr {
self.lang.get(sentence) self.lang.get(sentence)
} }
/// Translate the given sentence id according to the current language.
pub fn t_from_id(&self, sentence_id: i64) -> &'static str { pub fn t_from_id(&self, sentence_id: i64) -> &'static str {
self.lang.get_from_id(sentence_id) self.lang.get_from_id(sentence_id)
} }
/// Translate a sentence with parameters. /// Translate a sentence with parameters.
/// Placeholders "{}" are replaced in the same order as the given parameters.
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String { pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
let text = self.lang.get(sentence); let text = self.lang.get(sentence);
let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect(); let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect();
@ -118,20 +123,6 @@ impl Tr {
} }
} }
// #[macro_export]
// macro_rules! t {
// ($self:expr, $str:expr) => {
// $self.t($str)
// };
// ($self:expr, $str:expr, $( $x:expr ),+ ) => {
// {
// let mut result = $self.t($str);
// $( result = result.replacen("{}", &$x.to_string(), 1); )+
// result
// }
// };
// }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct StoredLanguage { struct StoredLanguage {
code: String, code: String,
@ -151,7 +142,7 @@ struct Language {
const UNABLE_TO_FIND_TRANSLATION_MESSAGE: &str = "Unable to find translation"; const UNABLE_TO_FIND_TRANSLATION_MESSAGE: &str = "Unable to find translation";
impl Language { impl Language {
pub fn from_stored_language(stored_language: StoredLanguage) -> Self { fn from_stored_language(stored_language: StoredLanguage) -> Self {
Self { Self {
code: stored_language.code, code: stored_language.code,
territory: stored_language.territory, territory: stored_language.territory,
@ -166,15 +157,14 @@ impl Language {
} }
} }
pub fn get<T>(&'static self, sentence: T) -> &'static str fn get<T>(&'static self, sentence: T) -> &'static str
where where
T: Borrow<Sentence>, T: Borrow<Sentence>,
{ {
let sentence_cloned: Sentence = sentence.borrow().clone(); self.get_from_id(*sentence.borrow() as i64)
self.get_from_id(sentence_cloned as i64)
} }
pub fn get_from_id(&'static self, sentence_id: i64) -> &'static str { fn get_from_id(&'static self, sentence_id: i64) -> &'static str {
let text: &str = match self.translation.get(sentence_id as usize) { let text: &str = match self.translation.get(sentence_id as usize) {
None => UNABLE_TO_FIND_TRANSLATION_MESSAGE, None => UNABLE_TO_FIND_TRANSLATION_MESSAGE,
Some(text) => text, Some(text) => text,
@ -187,6 +177,7 @@ impl Language {
} }
} }
/// Returns all available languages as a tuple (code, name).
pub fn available_languages() -> Vec<(&'static str, &'static str)> { pub fn available_languages() -> Vec<(&'static str, &'static str)> {
TRANSLATIONS TRANSLATIONS
.iter() .iter()
@ -194,6 +185,7 @@ pub fn available_languages() -> Vec<(&'static str, &'static str)> {
.collect() .collect()
} }
/// Returns all available codes.
pub fn available_codes() -> Vec<&'static str> { pub fn available_codes() -> Vec<&'static str> {
TRANSLATIONS.iter().map(|tr| tr.code.as_ref()).collect() TRANSLATIONS.iter().map(|tr| tr.code.as_ref()).collect()
} }

View file

@ -20,14 +20,13 @@ pub fn get_ip_and_user_agent(headers: &HeaderMap, remote_address: SocketAddr) ->
} }
pub fn get_url_from_host(host: &str) -> String { pub fn get_url_from_host(host: &str) -> String {
let port: Option<u16> = 'p: { let port: Option<u16> = {
let split_port: Vec<&str> = host.split(':').collect(); let split_port: Vec<&str> = host.split(':').collect();
if split_port.len() == 2 { if split_port.len() == 2 {
if let Ok(p) = split_port[1].parse::<u16>() { split_port[1].parse::<u16>().ok()
break 'p Some(p); } else {
} None
} }
None
}; };
format!( format!(
"http{}://{}", "http{}://{}",

View file

@ -1,7 +1,7 @@
{# Needed by the frontend toast module. #} {# Needed by the frontend toast module. #}
<div id="toasts"> <div id="toasts">
<div class="toast"> <div class="toast">
<img class="icon" width="24" height="24" alt="icon" src=""> <img class="icon" width="24" height="24" alt="icon" src="/static/success.svg">
<div class="content user-message"></div> <div class="content user-message"></div>
<span class="close button"></span> <span class="close button"></span>
</div> </div>

View file

@ -119,18 +119,19 @@ async fn sign_up() -> Result<(), Box<dyn Error>> {
let mut mock_email_service = utils::mock_email::MockEmailService::new(); let mut mock_email_service = utils::mock_email::MockEmailService::new();
mock_email_service mock_email_service
.expect_send_email() .expect_send_email()
.withf_st(move |email, _title, message| { .withf(|_email_sender, email_receiver, _title, _message| {
email_receiver == "president@spaceball.planet"
})
.times(1)
.returning_st(move |_email_sender, _email_receiver, _title, message| {
sscanf!( sscanf!(
message, message,
"Follow this link to confirm your inscription, http://127.0.0.1:8000{}", "Follow this link to confirm your inscription, http://127.0.0.1:8000{}",
*validation_url_clone.borrow_mut() *validation_url_clone.borrow_mut()
) )
.unwrap(); .unwrap();
println!("{}", message); Ok(())
email == "president@spaceball.planet" });
})
.times(1)
.returning(|_email, _title, _message| Ok(()));
let state = utils::common_state_with_email_service(Arc::new(mock_email_service)).await?; let state = utils::common_state_with_email_service(Arc::new(mock_email_service)).await?;
let server = TestServer::new(app::make_service(state))?; let server = TestServer::new(app::make_service(state))?;

View file

@ -9,7 +9,7 @@ mock! {
pub EmailService {} pub EmailService {}
#[async_trait] #[async_trait]
impl email::EmailServiceTrait for EmailService { impl email::EmailServiceTrait for EmailService {
async fn send_email(&self, email: &str, title: &str, message: &str) async fn send_email(&self, email_sender: &str, email_receiver: &str, title: &str, message: &str)
-> Result<(), email::Error>; -> Result<(), email::Error>;
} }
} }

View file

@ -240,10 +240,9 @@ pub struct ShoppingListItem {
/*** Misc ***/ /*** Misc ***/
pub fn to_string<T>(ron: T) -> String pub fn to_string<T>(ron: T) -> Result<String, ron::Error>
where where
T: Serialize, T: Serialize,
{ {
// TODO: handle'unwrap'. to_string_pretty(&ron, PrettyConfig::new())
to_string_pretty(&ron, PrettyConfig::new()).unwrap()
} }

View file

@ -2,7 +2,7 @@ use serde::Deserialize;
use strum::EnumCount; use strum::EnumCount;
#[repr(i64)] #[repr(i64)]
#[derive(Debug, Clone, EnumCount, Deserialize)] #[derive(Debug, Clone, Copy, EnumCount, Deserialize)]
pub enum Sentence { pub enum Sentence {
MainTitle = 0, MainTitle = 0,
CreateNewRecipe, CreateNewRecipe,

View file

@ -15,7 +15,10 @@ pub enum Error {
Gloo(#[from] gloo::net::Error), Gloo(#[from] gloo::net::Error),
#[error("RON Spanned error: {0}")] #[error("RON Spanned error: {0}")]
Ron(#[from] ron::error::SpannedError), RonSpanned(#[from] ron::error::SpannedError),
#[error("RON Error: {0}")]
Ron(#[from] ron::error::Error),
#[error("HTTP error: {0}")] #[error("HTTP error: {0}")]
Http(String), Http(String),
@ -40,7 +43,7 @@ where
{ {
let url = format!("/ron-api/{}", api_name); let url = format!("/ron-api/{}", api_name);
let request_builder = method_fn(&url).header(CONTENT_TYPE, CONTENT_TYPE_RON); let request_builder = method_fn(&url).header(CONTENT_TYPE, CONTENT_TYPE_RON);
send_req(request_builder.body(ron_api::to_string(body))?).await send_req(request_builder.body(ron_api::to_string(body)?)?).await
} }
async fn req_with_params<T, U>( async fn req_with_params<T, U>(