From 9df07bf3db5d689b6a206841d40cff8349c4ab39 Mon Sep 17 00:00:00 2001 From: Erin Nova Date: Sun, 21 Nov 2021 21:47:19 -0500 Subject: [PATCH] Restructure code, allow for order modification --- Cargo.lock | 235 ++++++++++++++++++++++++++++++++++---------- Cargo.toml | 7 +- src/api.rs | 183 ++++++++++++++++++---------------- src/authenticate.rs | 27 +++++ src/database.rs | 8 ++ src/main.rs | 17 +++- src/structures.rs | 16 ++- 7 files changed, 345 insertions(+), 148 deletions(-) create mode 100644 src/authenticate.rs diff --git a/Cargo.lock b/Cargo.lock index 178d4e4..382504d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,60 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + [[package]] name = "ahash" version = "0.7.6" @@ -25,15 +79,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -157,11 +202,20 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" dependencies = [ - "crypto-mac", + "crypto-mac 0.8.0", "digest", "opaque-debug", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.8.0" @@ -224,6 +278,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -242,11 +305,32 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" dependencies = [ + "aes-gcm", + "base64", + "hkdf", "percent-encoding", + "rand 0.8.4", + "sha2", + "subtle", "time", "version_check", ] +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + [[package]] name = "crc32fast" version = "1.2.1" @@ -310,6 +394,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + [[package]] name = "deflate" version = "0.8.6" @@ -383,19 +486,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "fallible_collections" version = "0.4.3" @@ -595,6 +685,16 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.11.3" @@ -648,6 +748,26 @@ dependencies = [ "libc", ] +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac 0.10.1", + "digest", +] + [[package]] name = "hound" version = "3.4.0" @@ -688,12 +808,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.14" @@ -998,6 +1112,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "paris" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4503608d3670c54b6dd45e47744545a6ee8d56226cfe5e0446c7ba4a08d8b9fc" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1070,8 +1190,7 @@ dependencies = [ "argon2", "bincode", "captcha", - "env_logger", - "log", + "paris", "rand 0.8.4", "rand_core 0.6.3", "rocket", @@ -1104,6 +1223,17 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.15" @@ -1294,8 +1424,6 @@ version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ - "aho-corasick", - "memchr", "regex-syntax", ] @@ -1506,6 +1634,19 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1675,15 +1816,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - [[package]] name = "thread_local" version = "1.1.3" @@ -1940,6 +2072,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "uuid" version = "0.8.2" @@ -2054,15 +2196,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index bfe8b4a..993f5fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rocket = { version = "0.5.0-rc.1", features = ["json"] } +rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] } serde = "1.0.130" bincode = "1.3.3" sled = "0.34.7" @@ -15,5 +15,6 @@ rand_core = { version = "0.6", features = ["std"] } uuid = { version = "0.8", features = ["serde", "v5"] } rand = "0.8.4" captcha = "0.0.8" -log = "0.4.0" -env_logger = "0.9.0" +#log = "0.4.0" +#env_logger = "0.9.0" +paris = { version = "1.5", features = ["macros"] } diff --git a/src/api.rs b/src/api.rs index 0b1b1f2..58725a2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,18 +1,14 @@ use rocket::serde::json::Json; -use rocket::{form::Form, State}; +use rocket::{form::Form, http::CookieJar, State}; use rand::Rng; use uuid::Uuid; +use crate::authenticate::*; use crate::database::*; use crate::structures::*; -use argon2::{ - password_hash::{PasswordHasher, SaltString}, - Argon2, -}; - -use std::env; +use argon2::{password_hash::SaltString, Argon2}; // Endpoint to add a new product #[post("/api/new", data = "")] @@ -24,18 +20,7 @@ pub fn new_product( ) -> Json { /* Need to rework the password mechanism so it's not stored in plaintext serverside */ - let password = request.password.as_bytes(); // Turn the provided passord into a bytes array - let hashed_password = argon2 - .hash_password(password, &salt.as_ref()) - .unwrap() - .to_string(); // Hash the password - let mut admin_password = env::var("ADMIN_PASSWD").unwrap(); // Get the set admin password from environment variables - admin_password = argon2 - .hash_password(admin_password.as_bytes(), &salt.as_ref()) - .unwrap() - .to_string(); // Hash the admin password - - if hashed_password == admin_password { + if check_password(&request.password, &salt, &argon2) { // Check if the password is correct let new_product: Product = Product { // Create a new Product object from the supplied info @@ -61,7 +46,7 @@ pub fn new_product( // Endpoint to place a new order #[post("/api/order", data = "")] -pub fn new_order(order: Form>) -> String { +pub fn new_order(cookies: &CookieJar<'_>, order: Form>) -> String { let mut rng = rand::thread_rng(); // Get a new random number let to_hash = format!( "{}{}{}", @@ -69,63 +54,107 @@ pub fn new_order(order: Form>) -> String { order.encryption_key.to_string(), rng.gen::().to_string() ); // Create a has from various components of the order + a random number - let db_order: Order = Order { - name: order.name.to_string(), - message: order.user_message.to_string(), - encryption_key: order.encryption_key.to_string(), - email: order.email.to_string(), - payment_type: order.payment_type.clone(), - uuid: Uuid::new_v5(&Uuid::NAMESPACE_X500, to_hash.as_bytes()), - status: OrderStatus::Submitted, - }; - make_order(&db_order); - format!( - "Thank you for ordering from Catgirl Pharmacy!\nYour order Uuid is {}", - db_order.uuid.to_hyphenated().to_string() - ) + + let user_token = cookies.get_private("token").unwrap(); + if user_token.value() == order.captcha { + let db_order: Order = Order { + name: order.name.to_string(), + message: order.user_message.to_string(), + encryption_key: order.encryption_key.to_string(), + email: order.email.to_string(), + payment_type: order.payment_type.clone(), + uuid: Uuid::new_v5(&Uuid::NAMESPACE_X500, to_hash.as_bytes()), + status: OrderStatus::Submitted, + }; + make_order(&db_order); + return format!( + "Thank you for ordering from Catgirl Pharmacy!\nYour order Uuid is {}", + db_order.uuid.to_hyphenated().to_string() + ); + } else { + return "Incorrect CAPTCHA answer.\nPlease try again!".to_string(); + } } +// Modify an order +#[post("/api/order/update", data = "")] +pub fn update_order( + argon2: &State, + salt: &State, + update: Form>, +) -> Json { + if check_password(&update.password, &salt, &argon2) { + let mut new_order = read_order(&update.order_uuid).unwrap().unwrap(); + new_order.status = update.status; + new_order.payment_type = update.payment_type; + make_order(&new_order); + return Json(Status { + status: "success".to_string(), + reason: "meowy".to_string(), + }); + } else { + return Json(Status { + status: "fail".to_string(), + reason: "wrong password".to_string(), + }); + } +} + +// Get the details of a specific order from it's Uuid #[get("/api/order/")] pub fn order_info(uuid: &str) -> Json { - let order = read_order(uuid).unwrap().unwrap(); - return Json(order); + let order = read_order(uuid).unwrap().unwrap(); // Read the order from the database + return Json(order); // Serialize it into json & return } +// Get all orders made #[post("/api/orders", data = "")] pub fn all_orders( auth: Form, argon2: &State, salt: &State, ) -> Result>, Json> { - let entered_password = auth.password.as_bytes(); - let hashed_password = argon2 - .hash_password(entered_password, &salt.as_ref()) - .unwrap() - .to_string(); - let mut admin_password = env::var("ADMIN_PASSWD").unwrap(); - admin_password = argon2 - .hash_password(admin_password.as_bytes(), &salt.as_ref()) - .unwrap() - .to_string(); - - if admin_password == hashed_password { - let all_orders = read_all_orders(); - let mut return_string = String::new(); - let mut orders_list: Vec = Vec::new(); + if check_password(&auth.password, &salt, &argon2) { + let all_orders = read_all_orders(); // Read all orders from the database + let mut orders_list: Vec = Vec::new(); // Create a new vector of Order's for x in all_orders { - return_string = format!("{}\n{:?}", return_string, x); - orders_list.push(x); + // Loop through all read orders + orders_list.push(x); // Push each entry to the vector } - return Ok(Json(orders_list)); + return Ok(Json(orders_list)); // Return the orders serialized as json } else { return Err(Json(Status { + // Fail on wrong password status: "fail".to_string(), reason: "wrong password".to_string(), })); } } +// Delete an order +#[post("/api/delete/", data = "")] +pub fn delete_order( + auth: Form, + order: &str, + argon2: &State, + salt: &State, +) -> Json { + if check_password(&auth.password, &salt, &argon2) { + remove_order(&order).unwrap(); + return Json(Status { + status: "success".to_string(), + reason: "deleted order".to_string(), + }); + } else { + return Json(Status { + status: "fail".to_string(), + reason: "wrong password".to_string(), + }); + } +} + +// Update fields of a product #[post("/api/update", data = "")] pub fn update_product( db: &State, @@ -133,29 +162,21 @@ pub fn update_product( salt: &State, update: Form>, ) -> Json { - let password = update.password.as_bytes(); - let hashed_password = argon2 - .hash_password(password, &salt.as_ref()) - .unwrap() - .to_string(); - let mut admin_password = env::var("ADMIN_PASSWD").unwrap(); - admin_password = argon2 - .hash_password(admin_password.as_bytes(), &salt.as_ref()) - .unwrap() - .to_string(); - - if admin_password == hashed_password { + if check_password(&update.password, &salt, &argon2) { match update.field { + // Check what field is being updated "price_usd" => { - let mut new_product = read_product(db, &update.product).unwrap().unwrap(); - new_product.price_usd = update.value; - register_product(db, &update.product, &new_product); + let mut new_product = read_product(db, &update.product).unwrap().unwrap(); // Read the product from the database + new_product.price_usd = update.value; // Change the price_usd value to the provided value + register_product(db, &update.product, &new_product); // Update the product in the database return Json(Status { + // Return a json success status status: "success".to_string(), reason: "price changed".to_string(), }); } "stock" => { + // Same as before, but with stock let mut new_product = read_product(db, &update.product).unwrap().unwrap(); new_product.stock = update.value as u32; register_product(db, &update.product, &new_product); @@ -165,33 +186,23 @@ pub fn update_product( }); } _ => { + // Fail because provided field wasn't found, return json return Json(Status { status: "fail".to_string(), reason: "field not found".to_string(), - }) + }); } } }; return Json(Status { + // Return JSON fail due to wrong password status: "fail".to_string(), reason: "wrong password".to_string(), }); } -#[get("/api/stock/")] -pub fn get_stock(db: &State, product: &str) -> String { - read_product(db, &product) - .unwrap() - .unwrap() - .stock - .to_string() -} - -#[get("/api/price/")] -pub fn get_price(db: &State, product: &str) -> String { - read_product(db, &product) - .unwrap() - .unwrap() - .price_usd - .to_string() +// Get all data about a product +#[get("/api/product/")] +pub fn get_product_info(db: &State, product: &str) -> Json { + return Json(read_product(db, &product).unwrap().unwrap()); } diff --git a/src/authenticate.rs b/src/authenticate.rs new file mode 100644 index 0000000..ae41f5d --- /dev/null +++ b/src/authenticate.rs @@ -0,0 +1,27 @@ +use argon2::{ + password_hash::{PasswordHasher, SaltString}, + Argon2, +}; + +use rocket::State; + +use std::env; + +pub fn check_password(password: &str, salt: &State, argon2: &State) -> bool { + let entered_password = password.as_bytes(); // Get the user entered password + let hashed_password = argon2 // Hash the password + .hash_password(entered_password, &salt.as_ref()) + .unwrap() + .to_string(); + let mut admin_password = env::var("ADMIN_PASSWD").unwrap(); // Get the provided admin password from the environment variable + admin_password = argon2 // Hash the admin password + .hash_password(admin_password.as_bytes(), &salt.as_ref()) + .unwrap() + .to_string(); + + if hashed_password == admin_password { + return true; + } else { + return false; + } +} diff --git a/src/database.rs b/src/database.rs index 3ac351e..fba158b 100644 --- a/src/database.rs +++ b/src/database.rs @@ -92,3 +92,11 @@ pub fn read_all_orders() -> Vec { } return all_orders; // Return the orders } + +// Remove an order from the database +pub fn remove_order(uuid: &str) -> std::result::Result, sled::Error> { + info!("Removing order with Uuid {}", uuid); + let db = sled::open("order_db").unwrap(); + + db.remove(uuid) +} diff --git a/src/main.rs b/src/main.rs index e408a31..0088d3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,10 @@ use rocket::fairing::{Fairing, Info, Kind}; use rocket::{http::Header, Request, Response}; #[macro_use] -extern crate log; +extern crate paris; mod api; +mod authenticate; mod captcha; mod database; mod structures; @@ -45,9 +46,14 @@ impl Fairing for CORS { // Launch rocket #[launch] fn rocket() -> _ { - env_logger::init(); + let mut log = paris::Logger::new(); + //env_logger::init(); info!("Starting up Rocket"); + let _config = sled::Config::default() + .use_compression(true) + .compression_factor(15); + rocket::build() .manage(database::open()) // Manage database state .manage(Argon2::default()) // Manage Argon2 state @@ -55,15 +61,16 @@ fn rocket() -> _ { .mount( "/", routes![ - get_stock, - get_price, new_order, + update_order, order_info, all_orders, new_product, update_product, return_captcha, - check_captcha + check_captcha, + get_product_info, + delete_order ], ) .attach(CORS) diff --git a/src/structures.rs b/src/structures.rs index 8766c93..60ef5fe 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -1,12 +1,21 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -// Struct for the urers answer to a captcha +// Struct for the users answer to a captcha #[derive(Clone, Debug, FromForm)] pub struct CaptchaAnswer { pub text: String, } +// Struct for modifying an order +#[derive(FromForm, Copy, Clone)] +pub struct OrderUpdate<'r> { + pub r#order_uuid: &'r str, + pub status: OrderStatus, + pub payment_type: PaymentOption, + pub r#password: &'r str, +} + // Return status #[derive(Serialize)] pub struct Status { @@ -47,6 +56,7 @@ pub struct OrderRequest<'r> { pub r#user_message: &'r str, pub r#encryption_key: &'r str, pub r#payment_type: PaymentOption, + pub r#captcha: &'r str, } // Struct for a stored order @@ -71,7 +81,7 @@ pub struct Update<'r> { } // Possible status states for an order -#[derive(Debug, PartialEq, FromFormField, Serialize, Clone, Deserialize)] +#[derive(Debug, PartialEq, FromFormField, Serialize, Clone, Copy, Deserialize)] pub enum OrderStatus { Submitted, Processing, @@ -80,7 +90,7 @@ pub enum OrderStatus { } // Options for payment -#[derive(Debug, PartialEq, FromFormField, Serialize, Clone, Deserialize)] +#[derive(Debug, PartialEq, FromFormField, Serialize, Deserialize, Clone, Copy)] pub enum PaymentOption { XMR, ETH,