From 97966ecd2b2f0a6c9d76b30e070d899dc951d5a0 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Thu, 20 Apr 2023 11:46:10 -0400 Subject: [PATCH] beamin and beamout --- Cargo.lock | 2 + api/Cargo.toml | 4 +- api/src/main.rs | 2 + api/src/routes/auth.rs | 0 api/src/routes/beamin.rs | 111 ++++++++++++++++++ api/src/routes/beamout.rs | 97 +++++++++++++++ api/src/routes/callback.rs | 2 + api/src/routes/mod.rs | 5 +- .../src/entity/mod.rs | 1 + .../src/entity/prelude.rs | 1 + .../src/entity/user_auth_realm.rs | 8 ++ .../src/entity/user_savefile.rs | 34 ++++++ api/starkingdoms_api_migration/src/lib.rs | 6 +- ...m20230420_144333_create_table_user_data.rs | 42 +++++++ client/index.html | 17 ++- protocol/src/api.rs | 3 +- server/Cargo.toml | 1 + server/src/api.rs | 65 +++++++++- server/src/handler.rs | 2 + server/src/main.rs | 4 + 20 files changed, 397 insertions(+), 10 deletions(-) delete mode 100644 api/src/routes/auth.rs create mode 100644 api/src/routes/beamin.rs create mode 100644 api/src/routes/beamout.rs create mode 100644 api/starkingdoms_api_entities/src/entity/user_savefile.rs create mode 100644 api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs diff --git a/Cargo.lock b/Cargo.lock index 251c4fa807dd05f71e23a6c185ca4f9b88022205..48b03eb57a580ad36aa992e36111f4c3d96a54d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3307,6 +3307,7 @@ dependencies = [ "serde", "sha2", "simple_logger", + "starkingdoms-protocol", "starkingdoms_api_entities", "starkingdoms_api_migration", "tera", @@ -3337,6 +3338,7 @@ dependencies = [ "nalgebra", "rand", "rapier2d-f64", + "reqwest", "serde", "serde_json", "simple_logger", diff --git a/api/Cargo.toml b/api/Cargo.toml index effe286f29fffc54238f0453d6a36a216fbf9ba7..0a93a9f849f4991c309ad4115dea3b62d5455c95 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -30,4 +30,6 @@ jwt = { version = "0.16", features = ["openssl"] } # Auth openssl = "0.10" # Auth reqwest = "0.11" # Auth hmac = "0.12.1" # Auth -sha2 = "0.10.6" # Auth \ No newline at end of file +sha2 = "0.10.6" # Auth + +starkingdoms-protocol = { version = "0.1.0", path = "../protocol" } \ No newline at end of file diff --git a/api/src/main.rs b/api/src/main.rs index 12f8d65d17c4835056268aa79cd614253c93ba6d..4fc9566d7b8a1ffee83558fd3865ec334c5e543c 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -66,6 +66,8 @@ async fn main() -> Result<(), Box> { .wrap(RequestIdentifier::with_generator(|| HeaderValue::from_str(&ulid::Ulid::new().to_string()).unwrap())) .service(routes::select_realm::select_realm) .service(routes::callback::callback) + .service(routes::beamin::beam_in) + .service(routes::beamout::beam_out) .service(actix_files::Files::new("/static", "static")) }).bind(CONFIG.server.bind)?.run().await?; diff --git a/api/src/routes/auth.rs b/api/src/routes/auth.rs deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/api/src/routes/beamin.rs b/api/src/routes/beamin.rs new file mode 100644 index 0000000000000000000000000000000000000000..7125cb6b53a2f83fe6995bf68efc4c15c95d9cb0 --- /dev/null +++ b/api/src/routes/beamin.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; +use actix_web::{HttpResponse, post}; +use actix_web::web::{Data, Json}; +use hmac::digest::KeyInit; +use hmac::Hmac; +use jwt::VerifyWithKey; +use log::error; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use starkingdoms_protocol::api::APISavedPlayerData; +use crate::AppState; +use crate::config::CONFIG; +use crate::error::{APIError, APIErrorsResponse}; + +#[derive(Serialize, Deserialize)] +pub struct BeaminRequest { + pub api_token: String, + pub user_auth_realm_id: String, + pub user_auth_token: String +} + +#[derive(Serialize, Deserialize)] +pub struct BeaminResponse { + pub save_id: String, + pub save: APISavedPlayerData +} + +#[post("/beamin")] +pub async fn beam_in(data: Json, state: Data) -> HttpResponse { + if !CONFIG.internal_tokens.contains(&data.api_token) { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_BAD_TOKEN".to_string(), + message: "Missing or invalid api token".to_string(), + path: None, + } + ], + }) + } + + let key: Hmac = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap(); + let token: BTreeMap = match data.user_auth_token.verify_with_key(&key) { + Ok(t) => t, + Err(e) => { + error!("verifying error: {}", e); + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_BAD_TOKEN".to_string(), + message: "Missing or invalid user token".to_string(), + path: None, + } + ], + }) + } + }; + + if !token.contains_key("user") || !token.contains_key("nonce") { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_BAD_TOKEN".to_string(), + message: "Missing or invalid user token (missing scopes)".to_string(), + path: None, + } + ], + }); + } + + let user_id = token.get("user").unwrap(); + + let user_savefile: Vec = match starkingdoms_api_entities::entity::user_savefile::Entity::find().filter( + starkingdoms_api_entities::entity::user_savefile::Column::User.eq(user_id)) + .order_by_desc(starkingdoms_api_entities::entity::user_savefile::Column::Timestamp).all(&state.conn).await { + Ok(sf) => sf, + Err(e) => { + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "Unable to fetch user savefiles".to_string(), + path: None, + } + ], + }); + } + }; + if user_savefile.is_empty() { + return HttpResponse::NoContent().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_NO_SAVES".to_string(), + message: "This user has no savefiles".to_string(), + path: None, + } + ], + }); + } + + let save = &user_savefile[0]; + let save_id = &save.id; + let save_data_str = &save.data; + let save_data: APISavedPlayerData = toml::from_str(save_data_str).expect("database contained corrupted player save data"); + + HttpResponse::Ok().json(BeaminResponse { + save_id: save_id.clone(), + save: save_data + }) +} \ No newline at end of file diff --git a/api/src/routes/beamout.rs b/api/src/routes/beamout.rs new file mode 100644 index 0000000000000000000000000000000000000000..02247910a0a6d6a9f088b955fbfbe37ae4b243fc --- /dev/null +++ b/api/src/routes/beamout.rs @@ -0,0 +1,97 @@ +use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::{HttpResponse, post}; +use actix_web::web::{Data, Json}; +use hmac::digest::KeyInit; +use hmac::Hmac; +use jwt::VerifyWithKey; +use log::error; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use ulid::Ulid; +use starkingdoms_protocol::api::APISavedPlayerData; +use crate::AppState; +use crate::config::CONFIG; +use crate::error::{APIError, APIErrorsResponse}; + +#[derive(Serialize, Deserialize)] +pub struct BeamoutRequest { + pub api_token: String, + pub user_auth_realm_id: String, + pub user_auth_token: String, + pub data: APISavedPlayerData +} + +#[derive(Serialize, Deserialize)] +pub struct BeamoutResponse {} + +#[post("/beamout")] +pub async fn beam_out(data: Json, state: Data) -> HttpResponse { + if !CONFIG.internal_tokens.contains(&data.api_token) { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_BAD_TOKEN".to_string(), + message: "Missing or invalid api token".to_string(), + path: None, + } + ], + }) + } + + let key: Hmac = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap(); + let token: BTreeMap = match data.user_auth_token.verify_with_key(&key) { + Ok(t) => t, + Err(e) => { + error!("verifying error: {}", e); + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_BAD_TOKEN".to_string(), + message: "Missing or invalid user token".to_string(), + path: None, + } + ], + }) + } + }; + + if !token.contains_key("user") || !token.contains_key("nonce") { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_BAD_TOKEN".to_string(), + message: "Missing or invalid user token (missing scopes)".to_string(), + path: None, + } + ], + }); + } + + let saved_data = toml::to_string(&data.data).unwrap(); + let savefile_model = starkingdoms_api_entities::entity::user_savefile::Model { + id: format!("save-{}", Ulid::new().to_string()), + user: token.get("user").unwrap().clone(), + data: saved_data, + timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, + }; + let savefile_active_model = savefile_model.into_active_model(); + match savefile_active_model.insert(&state.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "database failure".to_string(), + path: None, + } + ], + }); + } + } + + HttpResponse::Ok().json(BeamoutResponse {}) +} \ No newline at end of file diff --git a/api/src/routes/callback.rs b/api/src/routes/callback.rs index 55191ca8e8347af4b3c38c6cfa7e7ed6b45fcc20..5b110e1ad3d002d7b4b0ab4fdacde69411abf4f4 100644 --- a/api/src/routes/callback.rs +++ b/api/src/routes/callback.rs @@ -95,6 +95,7 @@ pub async fn callback(query: Query, state: Data) let key: Hmac = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap(); let mut claims = BTreeMap::new(); claims.insert("user", user.id.clone()); + claims.insert("nonce", Ulid::new().to_string()); let token_str = claims.sign_with_key(&key).unwrap(); let auth_url = format!("{}/?token={}&user={}", CONFIG.game, token_str, user.id); return HttpResponse::Found().append_header(("Location", auth_url)).finish(); @@ -116,6 +117,7 @@ pub async fn callback(query: Query, state: Data) let key: Hmac = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap(); let mut claims = BTreeMap::new(); claims.insert("user", new_user.id.clone()); + claims.insert("nonce", Ulid::new().to_string()); let token_str = claims.sign_with_key(&key).unwrap(); let auth_url = format!("{}/?token={}&user={}", CONFIG.game, token_str, new_user.id.clone()); diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index fa420debb85c2ff6db52fe0aba0b890ea034a486..dad8ba4f0227e26e1732f859e9be98335d7994b6 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -1,3 +1,4 @@ -pub mod auth; +pub mod beamin; pub mod select_realm; -pub mod callback; \ No newline at end of file +pub mod callback; +pub mod beamout; \ No newline at end of file diff --git a/api/starkingdoms_api_entities/src/entity/mod.rs b/api/starkingdoms_api_entities/src/entity/mod.rs index 123e944c44e1e0a24c1a5a2ca9baa0fc8ba97aed..396669ece549665ecd396a76f96848376dd060e1 100644 --- a/api/starkingdoms_api_entities/src/entity/mod.rs +++ b/api/starkingdoms_api_entities/src/entity/mod.rs @@ -4,3 +4,4 @@ pub mod prelude; pub mod user; pub mod user_auth_realm; +pub mod user_savefile; diff --git a/api/starkingdoms_api_entities/src/entity/prelude.rs b/api/starkingdoms_api_entities/src/entity/prelude.rs index b10b04c7501147b5b54792295df41569a4107f30..26cd28ab07655687eb68f8922b0e39dfb22742ff 100644 --- a/api/starkingdoms_api_entities/src/entity/prelude.rs +++ b/api/starkingdoms_api_entities/src/entity/prelude.rs @@ -2,3 +2,4 @@ pub use super::user::Entity as User; pub use super::user_auth_realm::Entity as UserAuthRealm; +pub use super::user_savefile::Entity as UserSavefile; diff --git a/api/starkingdoms_api_entities/src/entity/user_auth_realm.rs b/api/starkingdoms_api_entities/src/entity/user_auth_realm.rs index d039ec768dadef43c8a1372dd9157a95c9a70c57..510226438bf0390ea248f2cbca00f93ad1d2852d 100644 --- a/api/starkingdoms_api_entities/src/entity/user_auth_realm.rs +++ b/api/starkingdoms_api_entities/src/entity/user_auth_realm.rs @@ -22,6 +22,8 @@ pub enum Relation { on_delete = "NoAction" )] User, + #[sea_orm(has_many = "super::user_savefile::Entity")] + UserSavefile, } impl Related for Entity { @@ -30,4 +32,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserSavefile.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/api/starkingdoms_api_entities/src/entity/user_savefile.rs b/api/starkingdoms_api_entities/src/entity/user_savefile.rs new file mode 100644 index 0000000000000000000000000000000000000000..6c8e4fbf113450477a7690027eb98663ec57183f --- /dev/null +++ b/api/starkingdoms_api_entities/src/entity/user_savefile.rs @@ -0,0 +1,34 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "user_savefile")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + pub user: String, + pub data: String, + #[sea_orm(unique)] + pub timestamp: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user_auth_realm::Entity", + from = "Column::User", + to = "super::user_auth_realm::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + UserAuthRealm, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserAuthRealm.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/api/starkingdoms_api_migration/src/lib.rs b/api/starkingdoms_api_migration/src/lib.rs index 0dc8b72dd9018635e194bbeba026b6d091114489..32c20b6e4c0d81f682f179eaf6a5b3c54f1dbde0 100644 --- a/api/starkingdoms_api_migration/src/lib.rs +++ b/api/starkingdoms_api_migration/src/lib.rs @@ -1,7 +1,8 @@ pub use sea_orm_migration::prelude::*; -mod m20230417_162824_create_table_users; -mod m20230417_164240_create_table_user_auth_realms; +pub mod m20230417_162824_create_table_users; +pub mod m20230417_164240_create_table_user_auth_realms; +pub mod m20230420_144333_create_table_user_data; pub struct Migrator; @@ -11,6 +12,7 @@ impl MigratorTrait for Migrator { vec![ Box::new(m20230417_162824_create_table_users::Migration), Box::new(m20230417_164240_create_table_user_auth_realms::Migration), + Box::new(m20230420_144333_create_table_user_data::Migration), ] } } diff --git a/api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs b/api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7cb40080eda4b8e9f4b5ca20777a391486e7d34 --- /dev/null +++ b/api/starkingdoms_api_migration/src/m20230420_144333_create_table_user_data.rs @@ -0,0 +1,42 @@ +use sea_orm_migration::prelude::*; +use crate::m20230417_162824_create_table_users::User; +use crate::m20230417_164240_create_table_user_auth_realms::UserAuthRealm; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.create_table( + Table::create() + .table(UserSavefile::Table) + .col(ColumnDef::new(UserSavefile::Id).string().not_null().primary_key()) + .col(ColumnDef::new(UserSavefile::User).string().not_null()) + .col(ColumnDef::new(UserSavefile::Data).string().not_null()) + .col(ColumnDef::new(UserSavefile::Timestamp).big_unsigned().not_null().unique_key()) + .foreign_key( + ForeignKey::create() + .from(UserSavefile::Table, UserSavefile::User) + .to(UserAuthRealm::Table, UserAuthRealm::Id) + ) + .to_owned() + ).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(UserSavefile::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +pub enum UserSavefile { + Table, + Id, + User, + Data, + Timestamp +} diff --git a/client/index.html b/client/index.html index 17f1328b73a1e3010df3925bb1603a2bbc33f48c..05f6fb7c8d490ff1799fe94706a4654b5c665f73 100644 --- a/client/index.html +++ b/client/index.html @@ -28,11 +28,15 @@
+
+

You are not logged in.

+ + Click here to log in or change accounts. diff --git a/protocol/src/api.rs b/protocol/src/api.rs index 3614a351e6523278446a7bf5e114424678af0119..a79472e4349bdfe8aba0273fb4c0336b17fb935e 100644 --- a/protocol/src/api.rs +++ b/protocol/src/api.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +// ALL FIELDS **MUST** BE WRAPPED IN Option<> +#[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct APISavedPlayerData { } \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml index e11b94a074c58ffccf9b2762169d812a059accbf..2745396fa1805a2884683b6d07e6709407c616a5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -26,6 +26,7 @@ lazy_static = "1.4.0" rapier2d-f64 = { version = "0.17.2", features = [ "simd-stable" ] } nalgebra = "0.32.2" rand = "0.8.5" +reqwest = "0.11.16" [build-dependencies] cargo_metadata = "0.15" diff --git a/server/src/api.rs b/server/src/api.rs index 133b8be5d1b29c751180bd7db366a8e6cf982391..7c649ab68c9f3df65b71d24270f63085397f10a8 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -1,12 +1,71 @@ use std::error::Error; +use log::error; +use reqwest::StatusCode; +use serde::{Serialize, Deserialize}; use starkingdoms_protocol::api::APISavedPlayerData; +#[derive(Serialize, Deserialize)] +pub struct BeaminRequest { + pub api_token: String, + pub user_auth_realm_id: String, + pub user_auth_token: String +} + +#[derive(Serialize, Deserialize)] +pub struct BeaminResponse { + pub save_id: String, + pub save: APISavedPlayerData +} + pub async fn load_player_data_from_api(token: &str, user_id: &str, internal_token: &str) -> Result> { - // TODO - Ok(APISavedPlayerData {}) + let client = reqwest::Client::new(); + + let req_body = BeaminRequest { + api_token: internal_token.to_owned(), + user_auth_realm_id: user_id.to_owned(), + user_auth_token: token.to_owned() + }; + + let res = client.post(format!("{}/beamin", std::env::var("STK_API_URL").unwrap())).header("Content-Type", "application/json").body(serde_json::to_string(&req_body)?).send().await?; + + if res.status() == StatusCode::NO_CONTENT { + return Ok(APISavedPlayerData::default()) + } + + if res.status() != StatusCode::OK { + error!("error with API call (status: {}, body: {})", res.status(), res.text().await?); + return Err("Error with API call".into()) + } + + let resp: BeaminResponse = serde_json::from_str(&res.text().await?)?; + + Ok(resp.save) +} + +#[derive(Serialize, Deserialize)] +pub struct BeamoutRequest { + pub api_token: String, + pub user_auth_realm_id: String, + pub user_auth_token: String, + pub data: APISavedPlayerData } pub async fn save_player_data_to_api(data: &APISavedPlayerData, token: &str, user_id: &str, internal_token: &str) -> Result<(), Box> { - // TODO + let client = reqwest::Client::new(); + + let req_body = BeamoutRequest { + api_token: internal_token.to_owned(), + user_auth_realm_id: user_id.to_owned(), + user_auth_token: token.to_owned(), + data: data.to_owned() + }; + + let res = client.post(format!("{}/beamout", std::env::var("STK_API_URL").unwrap())).header("Content-Type", "application/json").body(serde_json::to_string(&req_body)?).send().await?; + + if res.status() != StatusCode::OK { + error!("error with API call (status: {}, body: {})", res.status(), res.text().await?); + return Err("Error with API call".into()) + } + Ok(()) } \ No newline at end of file diff --git a/server/src/handler.rs b/server/src/handler.rs index 740d51c80681b5a4cd9e2ff118e284ac9255dee6..13ad801cad63d780654b0cc02c80979a9cc5cca2 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -185,6 +185,8 @@ pub async fn handle_client(mgr: ClientManager, entities: Arc