1
0
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:
NG (Graham)
2026-02-25 21:19:12 -05:00
parent cab6774366
commit dfe95c2ee9
9 changed files with 402 additions and 13 deletions

5
Cargo.lock generated
View File

@@ -2623,10 +2623,15 @@ dependencies = [
"clap",
"env_logger",
"git-version",
"hex",
"jsonwebtoken",
"libfj",
"log",
"oj_rc_core",
"serde",
"serde_json",
"sha2",
"tokio",
]
[[package]]

View File

@@ -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" ] }

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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)
}
}

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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,
}
}))