diff --git a/Cargo.lock b/Cargo.lock index d615fc0..1905312 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2623,10 +2623,15 @@ dependencies = [ "clap", "env_logger", "git-version", + "hex", + "jsonwebtoken", "libfj", "log", + "oj_rc_core", "serde", "serde_json", + "sha2", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fc6d0d1..de20176 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,4 @@ hex = "0.4" base64 = "0.22" #oj_serdes = { version = "0.3.0", path = "../oj_core/serdes" } oj_serdes = "0.3.0" +jsonwebtoken = { version = "10", features = [ "rust_crypto" ] } diff --git a/rc_core/Cargo.toml b/rc_core/Cargo.toml index 45846b1..6d0d3cb 100644 --- a/rc_core/Cargo.toml +++ b/rc_core/Cargo.toml @@ -25,7 +25,7 @@ indexmap = { version = "2.0", features = ["serde"] } # auth libfj.workspace = true -jsonwebtoken = { version = "10", features = [ "rust_crypto" ] } +jsonwebtoken.workspace = true argon2 = { version = "0.5", features = [ "std" ] } # intercom diff --git a/rc_microtransactions/Cargo.toml b/rc_microtransactions/Cargo.toml index 2127eb8..6527d04 100644 --- a/rc_microtransactions/Cargo.toml +++ b/rc_microtransactions/Cargo.toml @@ -16,3 +16,10 @@ git-version.workspace = true clap.workspace = true serde.workspace = true serde_json.workspace = true + +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +# token transforming +jsonwebtoken.workspace = true +oj_rc_core = { version = "*", path = "../rc_core" } +sha2 = "0.10" +hex.workspace = true diff --git a/rc_microtransactions/src/main.rs b/rc_microtransactions/src/main.rs index 7b1b0ac..749c6f8 100644 --- a/rc_microtransactions/src/main.rs +++ b/rc_microtransactions/src/main.rs @@ -21,6 +21,7 @@ async fn main() -> std::io::Result<()> { let cli_args = cli::CliArgs::get(); let cli_args2 = actix_web::web::Data::new(cli_args.clone()); + let token_secret = actix_web::web::Data::new(robocraft::TokenSecret::load(&cli_args.data_robocraft).await?); HttpServer::new(move || { App::new() @@ -30,6 +31,7 @@ async fn main() -> std::io::Result<()> { srv.call(req) }) .app_data(cli_args2.clone()) + .app_data(token_secret.clone()) .service(index) .service(robocraft::robopay_store) .service(robocraft::robopay_token) diff --git a/rc_microtransactions/src/robocraft/auth_jwt.rs b/rc_microtransactions/src/robocraft/auth_jwt.rs new file mode 100644 index 0000000..82569ed --- /dev/null +++ b/rc_microtransactions/src/robocraft/auth_jwt.rs @@ -0,0 +1,110 @@ +use actix_web::{FromRequest, HttpRequest, dev::Payload}; + +#[derive(Clone)] +pub struct TokenSecret { + secret: Box<[u8]>, +} + +impl TokenSecret { + pub async fn load(root: impl AsRef) -> std::io::Result { + let token_path = root.as_ref().join(oj_rc_core::persist::user::TOKEN_SECRET_FILENAME); + let secret = tokio::fs::read(token_path).await?; + Ok(Self { + secret: secret.into_boxed_slice(), + }) + } +} + +#[allow(dead_code)] +pub struct PaymentToken { + pub data: libfj::robocraft::TokenPayload, + pub token: String, +} + +#[derive(Debug)] +pub enum TokenError { + NoHeader, + Invalid, + NoAuth, +} + +impl std::fmt::Display for TokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NoHeader => write!(f, "Missing authorization token"), + Self::Invalid => write!(f, "Invalid authorization token"), + Self::NoAuth => write!(f, "Invalid authorization secret"), + } + } +} + +impl actix_web::error::ResponseError for TokenError { + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::UNAUTHORIZED + } +} + +impl std::error::Error for TokenError {} + +impl FromRequest for PaymentToken { + type Error = TokenError; + type Future = PaymentTokenFuture; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let auth_header_opt = req.headers() + .get(actix_web::http::header::AUTHORIZATION); + let result = if let Some(auth_header) = auth_header_opt { + match auth_header.to_str() { + Ok(header_val) => { + if let Some((scheme, token)) = header_val.split_once(' ') { + match &scheme.to_lowercase() as &str { + "robocraft" => { + if let Some(secret) = req.app_data::>() { + let secret = jsonwebtoken::DecodingKey::from_secret(&secret.secret); + let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); + validation.set_required_spec_claims::<&str>(&[]); + let token_data_res = jsonwebtoken::decode::(token, &secret, &validation); + match token_data_res { + Ok(data) => Ok(PaymentToken { + data: data.claims, + token: token.to_owned() + }), + Err(_e) => Err(TokenError::Invalid), + } + } else { + Err(TokenError::NoAuth) + } + }, + _ => Err(TokenError::Invalid), + } + } else { + Err(TokenError::Invalid) + } + }, + Err(_e) => { + Err(TokenError::Invalid) + } + } + } else { + Err(TokenError::NoHeader) + }; + PaymentTokenFuture { + result: Some(result), + } + } +} + +pub struct PaymentTokenFuture { + result: Option>, +} + +impl core::future::Future for PaymentTokenFuture { + type Output = Result; + + fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll { + self.get_mut().result + .take() + .map(std::task::Poll::Ready) + .unwrap_or(std::task::Poll::Pending) + } +} diff --git a/rc_microtransactions/src/robocraft/mod.rs b/rc_microtransactions/src/robocraft/mod.rs index a49e185..3ff46cd 100644 --- a/rc_microtransactions/src/robocraft/mod.rs +++ b/rc_microtransactions/src/robocraft/mod.rs @@ -4,6 +4,9 @@ pub use store::robopay_store; mod token; pub use token::robopay_token; +mod auth_jwt; +pub use auth_jwt::{PaymentToken, TokenSecret}; + use serde::Serialize; #[derive(Serialize)] diff --git a/rc_microtransactions/src/robocraft/store.rs b/rc_microtransactions/src/robocraft/store.rs index f890ce6..6dd6d9b 100644 --- a/rc_microtransactions/src/robocraft/store.rs +++ b/rc_microtransactions/src/robocraft/store.rs @@ -58,27 +58,283 @@ enum StoreItemType { type ItemResponse = Response>; #[post("/robopay/store")] -pub async fn robopay_store(_cli: Data) -> Result, Error> { +pub async fn robopay_store(_cli: Data, _auth: super::PaymentToken) -> Result, Error> { // TODO authentication (Authorization header is "Robocraft {JWT token from auth}") Ok(Json(Response { response: ResponseData { data: vec![ + // Robopass + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::RoboPass, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "RoboPass".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "I wish :(".to_owned(), + old_currency_string: "Robopass :(".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + // Premium + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::PremiumForLife, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "PremiumPackLife".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "15.99/month".to_owned(), + old_currency_string: "Line go up".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: true, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Premium, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "PremiumPack1".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "0.99".to_owned(), + old_currency_string: "".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Premium, + amount: 3, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "PremiumPack2".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "1.99".to_owned(), + old_currency_string: "2.99".to_owned(), + price_for_check: 1.99, + old_price_for_check: 2.99, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Premium, + amount: 7, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "PremiumPack3".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "4.99".to_owned(), + old_currency_string: "6.7".to_owned(), + price_for_check: 4.99, + old_price_for_check: 6.7, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + // Cosmic credit StoreBundle { items: vec![ StoreItem { item_type: StoreItemType::CosmeticCredits, - amount: 1, + amount: 100, data: "RE_store_item_data_01".to_owned(), }, ], item_sku: "CosmeticCredits1".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets currency_code: "RE_no_currency".to_owned(), - currency_string: "RE_DO_NOT_BUY_01".to_owned(), - old_currency_string: "RE_DO_NOT_BUY_OLD_01".to_owned(), - price_for_check: 42.0, - old_price_for_check: 41.0, - additional_value: 999888777, + currency_string: "$0.99".to_owned(), + old_currency_string: "Macrotransactions".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::CosmeticCredits, + amount: 500, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "CosmeticCredits2".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "Give me money".to_owned(), + old_currency_string: "Open source".to_owned(), + price_for_check: 42.99, + old_price_for_check: 12.99, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::CosmeticCredits, + amount: 1_000, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "CosmeticCredits3".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "Capitalism".to_owned(), + old_currency_string: "Communism".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, most_popular: true, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::CosmeticCredits, + amount: 2_000, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "CosmeticCredits4".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "$42.99".to_owned(), + old_currency_string: "Math".to_owned(), + price_for_check: 42.99, + old_price_for_check: 0.1, + additional_value: 0, + most_popular: false, + best_value: true, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + // Robits + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Robits, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "RobitsBundle1".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "A few bills".to_owned(), + old_currency_string: "Saving".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Robits, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "RobitsBundle2".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "Some bills".to_owned(), + old_currency_string: "Green redstone".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Robits, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "RobitsBundle3".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "Bill Clinton".to_owned(), + old_currency_string: "Green cocaine".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, + best_value: false, + currency_type: "RE_price_type_01".to_owned(), + is_available: true, + is_owned: false, + }, + StoreBundle { + items: vec![ + StoreItem { + item_type: StoreItemType::Robits, + amount: 1, + data: "RE_store_item_data_01".to_owned(), + }, + ], + item_sku: "RobitsBundle4".to_owned(), // SKUs can be found in RealMoneyStoreExtraData of sharedassets2.assets + currency_code: "RE_no_currency".to_owned(), + currency_string: "All them bills".to_owned(), + old_currency_string: "Ribbits".to_owned(), + price_for_check: 1.0, + old_price_for_check: 1.0, + additional_value: 0, + most_popular: false, best_value: true, currency_type: "RE_price_type_01".to_owned(), is_available: true, diff --git a/rc_microtransactions/src/robocraft/token.rs b/rc_microtransactions/src/robocraft/token.rs index 98e87ca..25ebe1f 100644 --- a/rc_microtransactions/src/robocraft/token.rs +++ b/rc_microtransactions/src/robocraft/token.rs @@ -1,5 +1,6 @@ use actix_web::{web::{Data, Json}, Error, post}; use serde::{Deserialize, Serialize}; +use sha2::Digest; use super::Response; @@ -26,7 +27,7 @@ struct TokenRequest { #[derive(Serialize)] struct TokenResponse { - // url and data should be mutually exclusive + // TODO url and data should be mutually exclusive #[serde(skip_serializing_if = "Option::is_none")] url: Option, // open browser to URL #[serde(skip_serializing_if = "Option::is_none")] @@ -36,12 +37,16 @@ struct TokenResponse { type ItemResponse = Response; #[post("/robopay/token")] -pub async fn robopay_token(_cli: Data, body: Json) -> Result, Error> { - // TODO authentication (Authorization header is "Robocraft {JWT token from auth}") - log::debug!("robopay token post body: {:?}", body); +pub async fn robopay_token(_cli: Data, _body: Json, auth: super::PaymentToken) -> Result, Error> { + //log::debug!("robopay token post body: {:?}", body); + //log::debug!("robopay token post token: {:?}", auth.token); + //log::debug!("robopay token post user: {:?}", auth.data.public_id); + let hashed_token = sha2::Sha512::digest(auth.token.as_bytes()); + let hex_token = hex::encode(&hashed_token); + let tagged_url = format!("https://cheofoundation.donordrive.com/participants/64767?referrer=openjam&token={}", hex_token); Ok(Json(Response { response: TokenResponse { - url: Some("https://cncycle.cheofoundation.com/".to_owned()), + url: Some(tagged_url), data: None, } }))