mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Fill store page
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -2623,10 +2623,15 @@ dependencies = [
|
||||
"clap",
|
||||
"env_logger",
|
||||
"git-version",
|
||||
"hex",
|
||||
"jsonwebtoken",
|
||||
"libfj",
|
||||
"log",
|
||||
"oj_rc_core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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" ] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
110
rc_microtransactions/src/robocraft/auth_jwt.rs
Normal file
110
rc_microtransactions/src/robocraft/auth_jwt.rs
Normal file
@@ -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::path::Path>) -> std::io::Result<Self> {
|
||||
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::<actix_web::web::Data<TokenSecret>>() {
|
||||
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::<libfj::robocraft::TokenPayload>(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<Result<PaymentToken, TokenError>>,
|
||||
}
|
||||
|
||||
impl core::future::Future for PaymentTokenFuture {
|
||||
type Output = Result<PaymentToken, TokenError>;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
|
||||
self.get_mut().result
|
||||
.take()
|
||||
.map(std::task::Poll::Ready)
|
||||
.unwrap_or(std::task::Poll::Pending)
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -58,27 +58,283 @@ enum StoreItemType {
|
||||
type ItemResponse = Response<ResponseData<StoreBundle>>;
|
||||
|
||||
#[post("/robopay/store")]
|
||||
pub async fn robopay_store(_cli: Data<crate::cli::CliArgs>) -> Result<Json<ItemResponse>, Error> {
|
||||
pub async fn robopay_store(_cli: Data<crate::cli::CliArgs>, _auth: super::PaymentToken) -> Result<Json<ItemResponse>, 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,
|
||||
|
||||
@@ -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<String>, // open browser to URL
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -36,12 +37,16 @@ struct TokenResponse {
|
||||
type ItemResponse = Response<TokenResponse>;
|
||||
|
||||
#[post("/robopay/token")]
|
||||
pub async fn robopay_token(_cli: Data<crate::cli::CliArgs>, body: Json<TokenRequest>) -> Result<Json<ItemResponse>, 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<crate::cli::CliArgs>, _body: Json<TokenRequest>, auth: super::PaymentToken) -> Result<Json<ItemResponse>, 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,
|
||||
}
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user