From e3183eb238a49b62a06043282601bd4dec723fce Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sat, 2 May 2026 11:03:49 -0400 Subject: [PATCH] Create society service with basic functionality and auth #118 --- Cargo.lock | 185 +++++++++++- Cargo.toml | 1 + .../templates/rc_society/dashboard.html.hbs | 273 ++++++++++++++++++ assets/templates/rc_society/index.html.hbs | 233 +++++++++++++++ assets/templates/rc_society/login.html.hbs | 197 +++++++++++++ rc_core/src/persist/user/account_json.rs | 36 ++- rc_core/src/persist/user/common.rs | 8 + rc_core/src/persist/user/mod.rs | 3 +- rc_core/src/persist/user/multiplayer.rs | 12 - rc_core/src/persist/user/traits.rs | 14 +- rc_core/src/persist/user/web.rs | 4 + rc_multiplayer/src/disconnect.rs | 2 +- rc_multiplayer/src/events/activate_sync.rs | 2 +- .../src/events/all_loading_progress.rs | 2 +- rc_multiplayer/src/events/assist_bonus.rs | 2 +- .../src/events/client_unregister.rs | 2 +- rc_multiplayer/src/events/damage_bonus.rs | 2 +- rc_multiplayer/src/events/flipper_start.rs | 2 +- .../src/events/heal_assist_bonus.rs | 2 +- rc_multiplayer/src/events/heal_bonus.rs | 2 +- rc_multiplayer/src/events/kill_bonus.rs | 2 +- rc_multiplayer/src/events/kill_player.rs | 2 +- rc_multiplayer/src/events/loading_done.rs | 2 +- rc_multiplayer/src/events/loading_progress.rs | 2 +- rc_multiplayer/src/events/map_ping.rs | 2 +- rc_multiplayer/src/events/player_input.rs | 2 +- rc_multiplayer/src/events/player_leave.rs | 2 +- .../src/events/self_destruct_elimination.rs | 2 +- rc_multiplayer/src/events/spot_player.rs | 2 +- .../src/events/validate_game_guid.rs | 4 +- rc_multiplayer/src/events/weapon_select.rs | 2 +- rc_multiplayer/src/handler.rs | 2 +- .../src/handlers/gamemode_specific.rs | 2 +- .../src/handlers/ingame_broadcast.rs | 4 +- .../src/handlers/ingame_broadcast_dataless.rs | 4 +- rc_multiplayer/src/matches/aggregate.rs | 4 +- rc_multiplayer/src/matches/generic.rs | 14 +- rc_multiplayer/src/matches/messages.rs | 2 +- rc_multiplayer/src/matches/modes/pit.rs | 2 +- rc_multiplayer/src/vehicle_motion.rs | 2 +- rc_society/Cargo.toml | 28 ++ rc_society/README.md | 0 rc_society/run_debug.sh | 3 + rc_society/src/api/mod.rs | 0 rc_society/src/cli.rs | 58 ++++ rc_society/src/main.rs | 78 +++++ rc_society/src/web/dashboard.rs | 65 +++++ rc_society/src/web/favicon.rs | 11 + rc_society/src/web/index.rs | 77 +++++ rc_society/src/web/login.rs | 66 +++++ rc_society/src/web/mod.rs | 71 +++++ 51 files changed, 1424 insertions(+), 77 deletions(-) create mode 100644 assets/templates/rc_society/dashboard.html.hbs create mode 100644 assets/templates/rc_society/index.html.hbs create mode 100644 assets/templates/rc_society/login.html.hbs create mode 100644 rc_core/src/persist/user/web.rs create mode 100644 rc_society/Cargo.toml create mode 100644 rc_society/README.md create mode 100755 rc_society/run_debug.sh create mode 100644 rc_society/src/api/mod.rs create mode 100644 rc_society/src/cli.rs create mode 100644 rc_society/src/main.rs create mode 100644 rc_society/src/web/dashboard.rs create mode 100644 rc_society/src/web/favicon.rs create mode 100644 rc_society/src/web/index.rs create mode 100644 rc_society/src/web/login.rs create mode 100644 rc_society/src/web/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 24ee65e..91a456e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,7 +52,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64", + "base64 0.22.1", "bitflags", "brotli", "bytes", @@ -80,6 +80,22 @@ dependencies = [ "zstd", ] +[[package]] +name = "actix-identity" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810f47733f956175bd5b2ae17ae5237fa92bd1b6a4a65f646a7240dbe9ff2728" +dependencies = [ + "actix-service", + "actix-session", + "actix-utils", + "actix-web", + "derive_more", + "futures-core", + "serde", + "tracing", +] + [[package]] name = "actix-macros" version = "0.2.4" @@ -141,6 +157,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400c27fd4cdbe0082b7bbd29ac44a3070cbda1b2114138dc106ba39fe2f90dff" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more", + "rand 0.9.2", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -169,6 +202,7 @@ dependencies = [ "bytes", "bytestring", "cfg-if", + "cookie", "derive_more", "encoding_rs", "foldhash", @@ -224,6 +258,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -235,6 +279,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -341,6 +399,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "approx" version = "0.1.1" @@ -467,6 +531,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.22.1" @@ -800,6 +870,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "version_check", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -879,9 +967,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1422,6 +1520,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "git-version" version = "0.3.9" @@ -1678,7 +1786,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1955,7 +2063,7 @@ version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ - "base64", + "base64 0.22.1", "ed25519-dalek", "getrandom 0.2.17", "hmac", @@ -2006,7 +2114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68fff2e457fb18346f6de760a0bbfe2de69febc5ab1782c5ff470db14de2241d" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "cgmath 0.18.0", "chrono", "genmesh", @@ -2522,7 +2630,7 @@ version = "1.2.2" dependencies = [ "argon2", "async-trait", - "base64", + "base64 0.22.1", "chrono", "futures", "hex", @@ -2563,7 +2671,7 @@ name = "oj_rc_factory" version = "1.2.2" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "chrono", "hex", "libfj", @@ -2579,7 +2687,7 @@ version = "1.2.2" dependencies = [ "actix-files", "actix-web", - "base64", + "base64 0.22.1", "clap", "env_logger", "git-version", @@ -2772,6 +2880,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "oj_rc_society" +version = "1.2.2" +dependencies = [ + "actix-files", + "actix-identity", + "actix-session", + "actix-web", + "actix-ws", + "chrono", + "clap", + "cookie", + "env_logger", + "git-version", + "handlebars", + "libfj", + "log", + "oj_rc_core", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "oj_serdes" version = "0.3.0" @@ -2794,6 +2925,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "ordered-float" version = "4.6.0" @@ -2907,7 +3044,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -3038,6 +3175,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3429,7 +3578,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -4074,7 +4223,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bigdecimal", "bytes", "chrono", @@ -4154,7 +4303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", "bitflags", "byteorder", @@ -4201,7 +4350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", "bitflags", "byteorder", @@ -4748,6 +4897,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4760,7 +4919,7 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index fc1f57e..472db84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "rc_factory", "rc_factory_web", "rc_multiplayer", "rc_plugins", + "rc_society", ] [workspace.dependencies] diff --git a/assets/templates/rc_society/dashboard.html.hbs b/assets/templates/rc_society/dashboard.html.hbs new file mode 100644 index 0000000..c4b1211 --- /dev/null +++ b/assets/templates/rc_society/dashboard.html.hbs @@ -0,0 +1,273 @@ + + + + + + Dashboard - Openjam + + + + + + + + + + + + + + + + + + + + + + +
+
+ Welcome {{form.display_name}}! | + Home +
+

{{form.display_name}} Dashboard

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} + +
+
+

Details

+
+
Name
+
{{form.display_name}}
+
Public ID
+
{{form.public_id}}
+
Database ID
+
{{form.debug.user_id}}
+
Last Seen
+
+
Creation
+
+
+
+
+

Permissions

+
+
Moderator
+
{{form.perms.mod}}
+
Administrator
+
{{form.perms.admin}}
+
Developer
+
{{form.perms.dev}}
+
Royal
+
{{form.perms.royal}}
+
Banned
+
{{form.perms.banned}}
+
+
+
+

Garages

+
+
Total
+
{{form.debug.user_id}}/{{form.debug.user_id}}
+
+ +
+ View My Garages +
+
+
+
+

Factory

+
+
Total
+
{{form.debug.user_id}}/{{form.debug.user_id}}
+
+ +
+ View My Uploads +
+
+
+ +
+

Sanctions

+
+
Total
+
{{form.debug.user_id}}
+
+ +
+ View My Sanctions +
+
+
+
+

Social

+
+
Clan
+
{{form.display_name}}
+
Friends
+
{{form.debug.user_id}}
+
+
+
+

Federation

+
+
Enabled
+
{{form.perms.royal}}
+
+ +
+ Manage My Federation +
+
+
+
+ + +
+ + diff --git a/assets/templates/rc_society/index.html.hbs b/assets/templates/rc_society/index.html.hbs new file mode 100644 index 0000000..2d2a359 --- /dev/null +++ b/assets/templates/rc_society/index.html.hbs @@ -0,0 +1,233 @@ + + + + + + Home - Openjam + + + + + + + + + + + + + + + + + + + + + +
+
+ {{#if form.is_logged_in}} + Welcome {{form.display_name}}! | + Dashboard + {{else}} + Login + {{/if}} +
+

Welcome to OpenJam servers

+

FOSSifying proprietary games since 2025

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} + +
+

+ This service allows you to manage your account settings, import and export your garage bays, view the server config, and will eventually (not yet) federate with other OpenJam rc-servers instances. + You can view other known instances here. + This service is intended to have stable public APIs for expanding existing functionality. + If you do not want any of this functionality in your instance, simply do not run this service with your instance. +

+

+ Interested in running your own server instance? Check out the quickstart guide in the wiki. +

+
+ +
+
+

Server Details

+
+
Domain
+
{{form.server.domain}}
+
CDN
+
{{form.server.cdn}}
+
Auth
+
{{form.server.auth}}
+
Client Version
+
>={{form.server.min_version}}
+
Server Version
+
{{form.server.server_version}}
+
Start Time
+
+
+
+
+ Grid item 2 +
+
+ Grid item 3 +
+
+ Grid item 4 +
+
+ Grid item 5 +
+
+ Grid item 6 +
+
+ Grid item 7 +
+
+ Grid item 8 +
+
+ + + +
+ + diff --git a/assets/templates/rc_society/login.html.hbs b/assets/templates/rc_society/login.html.hbs new file mode 100644 index 0000000..0e97a06 --- /dev/null +++ b/assets/templates/rc_society/login.html.hbs @@ -0,0 +1,197 @@ + + + + + + Login - Openjam + + + + + + + + + + + + + + + + + + + + + +
+

Login to OpenJam servers

+

FOSSifying proprietary games since 2025

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} +
+
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+

By submitting, you agree to the privacy policy.

+
+ +
+ +
+ +
+

This project is not affiliated with FreeJam, Cardlife or Robocraft. We're just fixing their """sunset""".

+
+
+ + +
+ + diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 0dabdb5..6584556 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -81,16 +81,12 @@ impl AccountProvider { db: self.db.clone(), }) }*/ -} -#[async_trait::async_trait] -impl super::UserProvider for AccountProvider { - async fn authenticate(&self, token: super::UserToken) -> Result + Send + Sync>, super::AuthError> { - //let new_root = self.root.join(&token.uuid); + async fn auth_internal(&self, token: &str) -> Result { let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); validation.set_required_spec_claims::<&str>(&[]); - let token_data = jsonwebtoken::decode::(&token.token, &secret, &validation).map_err(|e| super::AuthError { + let token_data = jsonwebtoken::decode::(&token, &secret, &validation).map_err(|e| super::AuthError { message: e.to_string(), code: crate::data::error_codes::AuthErrorCode::BadCredentials, })?; @@ -119,8 +115,7 @@ impl super::UserProvider for AccountProvider { }; #[cfg(debug_assertions)] log::info!("Authenticated user {} with flags {:?}", display_name, token_data.claims.client_details.flags.as_slice()); - //let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?; - Ok(Box::new(UserData { + Ok(UserData { account: user_info, perms: user_perms, cubes: self.cubes.clone(), @@ -133,7 +128,14 @@ impl super::UserProvider for AccountProvider { http_client: std::sync::Arc::new(reqwest::Client::new()), db: self.db.clone(), secret: self.secret.clone(), - })) + }) + } +} + +#[async_trait::async_trait] +impl super::UserProvider for AccountProvider { + async fn authenticate(&self, token: super::UserToken) -> Result + Send + Sync>, super::AuthError> { + Ok(Box::new(self.auth_internal(&token.token).await?)) } async fn multiplayer_authenticate(&self, user: String) -> Result + Send + Sync>, super::AuthError> { @@ -174,6 +176,10 @@ impl super::UserProvider for AccountProvider { secret: self.secret.clone(), })) } + + async fn web_authenticate(&self, token: String) -> Result, super::AuthError> { + Ok(Box::new(self.auth_internal(&token).await?)) + } } #[async_trait::async_trait] @@ -349,6 +355,18 @@ impl super::UserAuthenticator for AccountProvider { async fn register(&self, info: super::RegistrationInfo) -> Result { super::register_new_user(&info, &self.db).await.map_err(|e| e.to_string()) } + + async fn verify(&self, token: String) -> Result { + let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret); + let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); + validation.set_required_spec_claims::<&str>(&[]); + jsonwebtoken::decode::(&token, &secret, &validation) + .map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }) + .map(|decoded| decoded.claims) + } } pub(super) struct UserData { diff --git a/rc_core/src/persist/user/common.rs b/rc_core/src/persist/user/common.rs index dadb5eb..8203bcf 100644 --- a/rc_core/src/persist/user/common.rs +++ b/rc_core/src/persist/user/common.rs @@ -14,6 +14,14 @@ impl super::CommonUser for UserData { &self.account.public_id } + fn display_name(&self) -> &'_ str { + &self.account.display_name + } + + fn creation(&self) -> i64 { + self.account.creation_time + } + fn is_mod(&self) -> bool { self.perms.moderator } diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 3d59c7e..e16c21b 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -11,7 +11,7 @@ mod inventory; pub use inventory::{UnlockedParts, UnlockOverride}; mod traits; -pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides}; +pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser}; pub mod intercom; pub use intercom::generate_token as generate_intercom_token; @@ -26,6 +26,7 @@ mod factory; mod userless; mod team; pub use team::{TeamChooser, StandardTeamChooser}; +mod web; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/multiplayer.rs b/rc_core/src/persist/user/multiplayer.rs index 566cd0e..ac3acb9 100644 --- a/rc_core/src/persist/user/multiplayer.rs +++ b/rc_core/src/persist/user/multiplayer.rs @@ -106,18 +106,6 @@ impl UserData { #[async_trait::async_trait] impl super::MultiplayerUser for UserData { - fn user_id(&self) -> i32 { - self.account.id - } - - fn user_name(&self) -> &'_ str { - &self.account.public_id - } - - fn display_name(&self) -> &'_ str { - &self.account.display_name - } - async fn current_game(&self) -> Result, super::MultiplayerError> { Ok(self.db.game_by_user_id_and_completion(self.account.id, false).await .map_err(|e| { diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 7dc01a3..977f6a4 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -63,6 +63,8 @@ pub trait UserProvider { async fn authenticate(&self, user: UserToken) -> Result + Send + Sync>, AuthError>; async fn multiplayer_authenticate(&self, user: String) -> Result + Send + Sync>, AuthError>; + + async fn web_authenticate(&self, token: String) -> Result, AuthError>; } #[async_trait::async_trait] @@ -70,6 +72,7 @@ pub trait UserAuthenticator { async fn login(&self, info: UserAuthInfo) -> Result; async fn user_exists(&self, user: UserId) -> Result; async fn register(&self, info: RegistrationInfo) -> Result; + async fn verify(&self, token: String) -> Result; } #[async_trait::async_trait] @@ -385,9 +388,6 @@ pub enum MultiplayerErrorCode { #[async_trait::async_trait] pub trait MultiplayerUser: IntercomUser + CommonUser { - fn user_id(&self) -> i32; - fn user_name(&self) -> &'_ str; - fn display_name(&self) -> &'_ str; async fn current_game(&self) -> Result, MultiplayerError>; async fn game_players(&self, guid: &str) -> Result, MultiplayerError>; async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>; @@ -455,6 +455,9 @@ pub trait CommonUser: Send + Sync { fn account_id(&self) -> i32; async fn resolve_config_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result; fn public_id(&self) -> &'_ str; + fn display_name(&self) -> &'_ str; + /// Seconds since Unix epoch + fn creation(&self) -> i64; fn is_mod(&self) -> bool; fn is_admin(&self) -> bool; fn is_dev(&self) -> bool; @@ -671,6 +674,11 @@ pub trait FactoryUser { async fn rate_vehicle(&self, slot: i32, combat: i32, cosmetic: i32) -> Result, polariton_server::operations::SimpleOpError>; } +#[async_trait::async_trait] +pub trait WebUser: CommonUser { + +} + #[async_trait::async_trait] pub trait Userless: Send + Sync { async fn lobby_state_listener(&self) -> Result, reqwest_websocket::Error>; diff --git a/rc_core/src/persist/user/web.rs b/rc_core/src/persist/user/web.rs new file mode 100644 index 0000000..aa2d0e9 --- /dev/null +++ b/rc_core/src/persist/user/web.rs @@ -0,0 +1,4 @@ +#[async_trait::async_trait] +impl super::WebUser for super::account_json::UserData { + // TODO +} diff --git a/rc_multiplayer/src/disconnect.rs b/rc_multiplayer/src/disconnect.rs index 7aec423..b06afbf 100644 --- a/rc_multiplayer/src/disconnect.rs +++ b/rc_multiplayer/src/disconnect.rs @@ -19,7 +19,7 @@ impl crate::DisconnectHandler for ClientDisconnecter { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData) { if let Some(user_info) = user.user().await { crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::EndConnection { - user_id: user_info.user_id(), + user_id: user_info.account_id(), is_unregister: false, }).await); } else { diff --git a/rc_multiplayer/src/events/activate_sync.rs b/rc_multiplayer/src/events/activate_sync.rs index b7f848d..11ae9e6 100644 --- a/rc_multiplayer/src/events/activate_sync.rs +++ b/rc_multiplayer/src/events/activate_sync.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for RequestLoadingSync { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLoadingSync { - user_id: user_info.user_id(), + user_id: user_info.account_id(), }).await); } else { log::error!("Failed to handle sync loading request for unknown user"); diff --git a/rc_multiplayer/src/events/all_loading_progress.rs b/rc_multiplayer/src/events/all_loading_progress.rs index cb565fd..a8c2ab1 100644 --- a/rc_multiplayer/src/events/all_loading_progress.rs +++ b/rc_multiplayer/src/events/all_loading_progress.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for RequestAllLoadingProgress { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLoadingProgress { - user_id: user_info.user_id(), + user_id: user_info.account_id(), }).await); } else { log::error!("Failed to broadcast loading progress for unknown user"); diff --git a/rc_multiplayer/src/events/assist_bonus.rs b/rc_multiplayer/src/events/assist_bonus.rs index fcdbf8e..13120d9 100644 --- a/rc_multiplayer/src/events/assist_bonus.rs +++ b/rc_multiplayer/src/events/assist_bonus.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for AssistBonus { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::AssistBonus { - user_id: user_info.user_id(), + user_id: user_info.account_id(), shootee: data.requester_player_id as u8, shooters: data.player_ids.into_iter().map(|x| x.player).collect(), }).await); diff --git a/rc_multiplayer/src/events/client_unregister.rs b/rc_multiplayer/src/events/client_unregister.rs index 120d836..3ea61c5 100644 --- a/rc_multiplayer/src/events/client_unregister.rs +++ b/rc_multiplayer/src/events/client_unregister.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for ClientUnregisterer { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::EndConnection { - user_id: user_info.user_id(), + user_id: user_info.account_id(), is_unregister: true, }).await); } else { diff --git a/rc_multiplayer/src/events/damage_bonus.rs b/rc_multiplayer/src/events/damage_bonus.rs index ee44f9f..e2a14fe 100644 --- a/rc_multiplayer/src/events/damage_bonus.rs +++ b/rc_multiplayer/src/events/damage_bonus.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for DamageBonus { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::DestroyCubesBonus { - user_id: user_info.user_id(), + user_id: user_info.account_id(), info: data, }).await); } diff --git a/rc_multiplayer/src/events/flipper_start.rs b/rc_multiplayer/src/events/flipper_start.rs index 546678c..e32f411 100644 --- a/rc_multiplayer/src/events/flipper_start.rs +++ b/rc_multiplayer/src/events/flipper_start.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for RectifierStart { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::FlippingStarted { - user_id: user_info.user_id(), + user_id: user_info.account_id(), }).await); } else { log::error!("Failed to handle sync loading request for unknown user"); diff --git a/rc_multiplayer/src/events/heal_assist_bonus.rs b/rc_multiplayer/src/events/heal_assist_bonus.rs index 857ce99..aa3eecd 100644 --- a/rc_multiplayer/src/events/heal_assist_bonus.rs +++ b/rc_multiplayer/src/events/heal_assist_bonus.rs @@ -23,7 +23,7 @@ impl crate::handlers::RlnlEventCodeHandler for HealAssistBonus { if let Some(user_info) = user.user().await { //log::info!("Heal assist for player {}", data.healing_player_id); super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::HealAssistBonus { - user_id: user_info.user_id(), + user_id: user_info.account_id(), healer: data.healing_player_id, healee: data.healed_player_id, }).await); diff --git a/rc_multiplayer/src/events/heal_bonus.rs b/rc_multiplayer/src/events/heal_bonus.rs index 9309004..41d9340 100644 --- a/rc_multiplayer/src/events/heal_bonus.rs +++ b/rc_multiplayer/src/events/heal_bonus.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for HealBonus { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::HealCubesBonus { - user_id: user_info.user_id(), + user_id: user_info.account_id(), info: data, }).await); } diff --git a/rc_multiplayer/src/events/kill_bonus.rs b/rc_multiplayer/src/events/kill_bonus.rs index 398903f..d997e00 100644 --- a/rc_multiplayer/src/events/kill_bonus.rs +++ b/rc_multiplayer/src/events/kill_bonus.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for KillEnemyBonus { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::KillBonus { - user_id: user_info.user_id(), + user_id: user_info.account_id(), shootee: data.killee_player_id, shooter: data.killer_player_id, }).await); diff --git a/rc_multiplayer/src/events/kill_player.rs b/rc_multiplayer/src/events/kill_player.rs index 392669f..c040a9f 100644 --- a/rc_multiplayer/src/events/kill_player.rs +++ b/rc_multiplayer/src/events/kill_player.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for KillEnemy { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::DestroyVehicle { - user_id: user_info.user_id(), + user_id: user_info.account_id(), remote_player: data.killee_player_id, killer_player: data.killer_player_id, }).await); diff --git a/rc_multiplayer/src/events/loading_done.rs b/rc_multiplayer/src/events/loading_done.rs index 292e5e3..d01769d 100644 --- a/rc_multiplayer/src/events/loading_done.rs +++ b/rc_multiplayer/src/events/loading_done.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for LoadComplete { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::LoadComplete { - user_id: user_info.user_id(), + user_id: user_info.account_id(), }).await); } else { log::error!("Failed to handle sync loading request for unknown user"); diff --git a/rc_multiplayer/src/events/loading_progress.rs b/rc_multiplayer/src/events/loading_progress.rs index 3505022..c205985 100644 --- a/rc_multiplayer/src/events/loading_progress.rs +++ b/rc_multiplayer/src/events/loading_progress.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for GameLoadingProgress { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::LoadingProgress { - user_id: user_info.user_id(), + user_id: user_info.account_id(), user_name: data.user_name.0, progress: data.progress, }).await); diff --git a/rc_multiplayer/src/events/map_ping.rs b/rc_multiplayer/src/events/map_ping.rs index b565315..3d3c59a 100644 --- a/rc_multiplayer/src/events/map_ping.rs +++ b/rc_multiplayer/src/events/map_ping.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for PingMap { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::MapPing { - user_id: user_info.user_id(), + user_id: user_info.account_id(), ping: data, }).await); } diff --git a/rc_multiplayer/src/events/player_input.rs b/rc_multiplayer/src/events/player_input.rs index 0221e4d..4359f88 100644 --- a/rc_multiplayer/src/events/player_input.rs +++ b/rc_multiplayer/src/events/player_input.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for PlayerInput { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::PlayerInputChanged { - user_id: user_info.user_id(), + user_id: user_info.account_id(), data, }).await); } else { diff --git a/rc_multiplayer/src/events/player_leave.rs b/rc_multiplayer/src/events/player_leave.rs index 1de37cc..0e7dec8 100644 --- a/rc_multiplayer/src/events/player_leave.rs +++ b/rc_multiplayer/src/events/player_leave.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for PlayerQuit { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLeave { - user_id: user_info.user_id(), + user_id: user_info.account_id(), }).await); } else { log::error!("Failed to handle sync loading request for unknown user"); diff --git a/rc_multiplayer/src/events/self_destruct_elimination.rs b/rc_multiplayer/src/events/self_destruct_elimination.rs index 55b9bc6..4a2cb59 100644 --- a/rc_multiplayer/src/events/self_destruct_elimination.rs +++ b/rc_multiplayer/src/events/self_destruct_elimination.rs @@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for SelfDestructElim { async fn handle(&self, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::SelfDestruct { - user_id: user_info.user_id(), + user_id: user_info.account_id(), is_classic: true, }).await); } else { diff --git a/rc_multiplayer/src/events/spot_player.rs b/rc_multiplayer/src/events/spot_player.rs index 1159e15..60301b3 100644 --- a/rc_multiplayer/src/events/spot_player.rs +++ b/rc_multiplayer/src/events/spot_player.rs @@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for SpotEnemy { async fn handle(&self, data: Self::In, _peer: &std::sync::Arc>, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::SpotVehicle { - user_id: user_info.user_id(), + user_id: user_info.account_id(), remote_player: data.player_id, }).await); } diff --git a/rc_multiplayer/src/events/validate_game_guid.rs b/rc_multiplayer/src/events/validate_game_guid.rs index 7603a27..844e9df 100644 --- a/rc_multiplayer/src/events/validate_game_guid.rs +++ b/rc_multiplayer/src/events/validate_game_guid.rs @@ -64,7 +64,7 @@ impl crate::handlers::RlnlEventCodeHandler for AuthUserGame { } }, Ok(None) => { - log::warn!("Cannot validate game guid for user {} with no ongoing game [disconnecting...]", user_info.user_id()); + log::warn!("Cannot validate game guid for user {} with no ongoing game [disconnecting...]", user_info.account_id()); super::log_lnl_send_failure(crate::handlers::RlnlSender::new(sender) .send_data(&rlnl::types::StringCode { ty: rlnl::types::GameServerErrorCodes::StrErrIncorrectGameGuid, @@ -76,7 +76,7 @@ impl crate::handlers::RlnlEventCodeHandler for AuthUserGame { peer.disconnect(); }, Err(e) => { - log::error!("Failed to get current game for user {}: {} [disconnecting...]", user_info.user_id(), e.message); + log::error!("Failed to get current game for user {}: {} [disconnecting...]", user_info.account_id(), e.message); super::log_lnl_send_failure(crate::handlers::RlnlSender::new(sender) .send_data(&rlnl::types::StringCode { ty: core_to_rlnl_mp_error_code(e.code), diff --git a/rc_multiplayer/src/events/weapon_select.rs b/rc_multiplayer/src/events/weapon_select.rs index 1c3ae76..8015d6e 100644 --- a/rc_multiplayer/src/events/weapon_select.rs +++ b/rc_multiplayer/src/events/weapon_select.rs @@ -24,7 +24,7 @@ impl crate::handlers::RlnlEventCodeHandler for WeaponSelect { if let Some(category) = oj_rc_core::data::weapon_list::ItemCategory::from_smaller(data.item_category as _) { if let Some(tier) = oj_rc_core::data::cube_list::ItemTier::from_u32(data.item_size as _) { super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::WeaponSelect { - user_id: user_info.user_id(), + user_id: user_info.account_id(), machine_id: data.machine_id, category, size: tier, diff --git a/rc_multiplayer/src/handler.rs b/rc_multiplayer/src/handler.rs index e396221..eca1ba2 100644 --- a/rc_multiplayer/src/handler.rs +++ b/rc_multiplayer/src/handler.rs @@ -79,7 +79,7 @@ impl literustlib_server::EventHandler for LnlEventHandler { async fn on_disconnect(&self, peer: &std::sync::Arc>, user: &Self::UserData) { self.disconnect_handler.handle(peer, user).await; if let Some(user_info) = user.user().await { - log::info!("Disconnect from user {} ({})", user_info.user_id(), peer.id()); + log::info!("Disconnect from user {} ({})", user_info.account_id(), peer.id()); } else { log::debug!("Disconnect from connection {}", peer.id()); } diff --git a/rc_multiplayer/src/handlers/gamemode_specific.rs b/rc_multiplayer/src/handlers/gamemode_specific.rs index 7c65dd4..b83200e 100644 --- a/rc_multiplayer/src/handlers/gamemode_specific.rs +++ b/rc_multiplayer/src/handlers/gamemode_specific.rs @@ -29,7 +29,7 @@ impl >, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::CustomLogicRlnl { - user_id: user_info.user_id(), + user_id: user_info.account_id(), event: self.event, property: self.property, data: Box::new(data), diff --git a/rc_multiplayer/src/handlers/ingame_broadcast.rs b/rc_multiplayer/src/handlers/ingame_broadcast.rs index 375b902..689e2f7 100644 --- a/rc_multiplayer/src/handlers/ingame_broadcast.rs +++ b/rc_multiplayer/src/handlers/ingame_broadcast.rs @@ -30,7 +30,7 @@ impl GenericGamemodeEngine { pub(super) async fn rebroadcast(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T, in_game: bool) { for (player_id, conn) in self.users.read().await.iter() { - if user_id == conn.user.user_id() { continue; } + if user_id == conn.user.account_id() { continue; } if conn.aliases.contains(player_id) { continue; } if in_game { let mode = ConnectionMode::from_u8(self.user_descriptor(*player_id).unwrap().state.mode.load(std::sync::atomic::Ordering::Relaxed)); @@ -388,7 +388,7 @@ impl GenericGamemodeEngine { pub(super) async fn rebroadcast_dataless(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, in_game: bool) { for (player_id, conn) in self.users.read().await.iter() { - if user_id == conn.user.user_id() { continue; } + if user_id == conn.user.account_id() { continue; } if conn.aliases.contains(player_id) { continue; } if in_game { let mode = ConnectionMode::from_u8(self.user_descriptor(*player_id).unwrap().state.mode.load(std::sync::atomic::Ordering::Relaxed)); @@ -586,7 +586,7 @@ impl GenericGamemodeEngine { } else { //tokio::time::sleep(std::time::Duration::from_secs(1)).await; //let id = users.len() as u8; - let user_id = user.user_id(); + let user_id = user.account_id(); let player_info_opt = self.descriptors.values().find(|p| p.descriptor.user_id == Some(user_id)); if player_info_opt.is_none() { log::warn!("User {} tried to connect to match {} which they are not in", user_id, self.game_guid()); @@ -617,7 +617,7 @@ impl GenericGamemodeEngine { literustlib::packet::Property::ReliableOrdered, &new_user.connection.connection ).await); - log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid); + log::debug!("User {} is validated to play game {}", new_user.user.account_id(), game_guid); let new_user = std::sync::Arc::new(new_user); if let Err(e) = new_user.user.save_player_connected_status(self.game_guid(), true).await { log::error!("Failed to mark player {} (user {}) as connected to game {}: {}", id, user_id, self.game_guid(), e); @@ -757,7 +757,7 @@ impl GenericGamemodeEngine { }; for (player_id, conn) in self.users.read().await.iter() { let user_desc = self.user_descriptor(*player_id).unwrap(); - if user_id == conn.user.user_id() { + if user_id == conn.user.account_id() { let progress_percent = ((progress * 100.0).ceil() as u8).clamp(0, 100); log::debug!("User {} is loaded {}% into game {}", user_id, progress_percent, self.game_guid()); user_desc.state.progress.store(progress_percent, std::sync::atomic::Ordering::Relaxed); @@ -894,7 +894,7 @@ impl GenericGamemodeEngine { for (player_id, user) in self.users.read().await.iter() { let user_desc = self.user_descriptor(*player_id).unwrap(); if user.aliases.contains(player_id) { continue; } - if user.user.user_id() == user_id { + if user.user.account_id() == user_id { if !matches!(ConnectionMode::from_u8(user_desc.state.mode.load(std::sync::atomic::Ordering::Relaxed)), ConnectionMode::Loading | ConnectionMode::Disconnected) { log::warn!("Got RequestLoadingSync after user {} was already in/past WaitingForSync stage", user_id); continue; @@ -1361,7 +1361,7 @@ impl GenericGamemodeEngine { fn spawn_send_loading_events(&self, user: &UserConnection, player_id: u8, players: Vec>, client_ais: Vec) { let connection = user.connection.clone(); - let user_id = user.user.user_id(); + let user_id = user.user.account_id(); tokio::spawn(Self::send_loading_events_wrapper(connection, player_id, user_id, players, client_ais)); } diff --git a/rc_multiplayer/src/matches/messages.rs b/rc_multiplayer/src/matches/messages.rs index a28c46c..b7a989e 100644 --- a/rc_multiplayer/src/matches/messages.rs +++ b/rc_multiplayer/src/matches/messages.rs @@ -120,7 +120,7 @@ impl GameMessage { pub fn user_id(&self) -> i32 { match self { Self::NewConnection { user, .. } => { - user.user_id() + user.account_id() } Self::EndConnection { user_id, .. } => *user_id, Self::RequestLeave { user_id, .. } => *user_id, diff --git a/rc_multiplayer/src/matches/modes/pit.rs b/rc_multiplayer/src/matches/modes/pit.rs index 47cdcb5..9a0ac0f 100644 --- a/rc_multiplayer/src/matches/modes/pit.rs +++ b/rc_multiplayer/src/matches/modes/pit.rs @@ -411,7 +411,7 @@ impl CustomGameLogic for PitLogic { } } if single_client.is_some() { - let user_id = read_lock.values().next().unwrap().user.user_id(); + let user_id = read_lock.values().next().unwrap().user.account_id(); let player_id = generic.user_key_by_user_id(user_id).unwrap(); let user_info = generic.user_descriptor(player_id).unwrap(); WinTracker::do_win(generic, self, user_info.descriptor.team as u8).await; diff --git a/rc_multiplayer/src/vehicle_motion.rs b/rc_multiplayer/src/vehicle_motion.rs index a58c104..08b924d 100644 --- a/rc_multiplayer/src/vehicle_motion.rs +++ b/rc_multiplayer/src/vehicle_motion.rs @@ -24,7 +24,7 @@ impl crate::RobotMotionHandler for VehicleMotionHandler { match rlnl::machine_motion::MachineMotion::byte_deserialize(&mut des) { Ok(motion_data) => { crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::Motion { - user_id: user_info.user_id(), + user_id: user_info.account_id(), motion: motion_data, }).await); }, diff --git a/rc_society/Cargo.toml b/rc_society/Cargo.toml new file mode 100644 index 0000000..cb40be6 --- /dev/null +++ b/rc_society/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "oj_rc_society" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +actix-web = { workspace = true, features = [ "secure-cookies" ] } +actix-session = { version = "0.11", features = [ "cookie-session" ], default-features = false } +actix-identity = "0.9" +cookie = "0.16" +actix-files.workspace = true +actix-ws = "0.3" +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "rt-multi-thread" ] } +#futures.workspace = true +clap.workspace = true +handlebars = { version = "6", features = ["dir_source"] } +oj_rc_core = { version = "*", path = "../rc_core" } +libfj.workspace = true +git-version.workspace = true +serde.workspace = true +serde_json.workspace = true +chrono.workspace = true diff --git a/rc_society/README.md b/rc_society/README.md new file mode 100644 index 0000000..e69de29 diff --git a/rc_society/run_debug.sh b/rc_society/run_debug.sh new file mode 100755 index 0000000..a6e46e5 --- /dev/null +++ b/rc_society/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_LOG=debug cargo run diff --git a/rc_society/src/api/mod.rs b/rc_society/src/api/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/rc_society/src/cli.rs b/rc_society/src/cli.rs new file mode 100644 index 0000000..aca3dea --- /dev/null +++ b/rc_society/src/cli.rs @@ -0,0 +1,58 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 8002)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Assets root + #[arg(long, default_value_t = {"../assets/robocraft".to_string()})] + pub assets_robocraft: String, + + /// User data root + #[arg(long, default_value_t = {"../data/robocraft".to_string()})] + pub data_robocraft: String, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } + + pub fn loaded(&self) -> LoadedArgs { + let assets_path = std::path::PathBuf::from(&self.assets_robocraft); + let data_path = std::path::PathBuf::from(&self.data_robocraft); + let token_path = data_path.join(oj_rc_core::persist::user::TOKEN_SECRET_FILENAME); + let secret = std::fs::read(&token_path).expect("Bad token"); + let cookie_key = if secret.len() < 32 { + log::warn!("{} should be >= 32 bytes (extending with zeroes)", token_path.display()); + let mut secret_ext = secret.clone(); + while secret_ext.len() < 32 { + secret_ext.push(0); + } + actix_web::cookie::Key::derive_from(&secret_ext) + } else { + actix_web::cookie::Key::derive_from(&secret) + }; + LoadedArgs { + secret: std::sync::Arc::new(secret.clone()), + cookie_key, + assets: assets_path, + data: data_path, + } + } +} + +#[allow(dead_code)] +pub struct LoadedArgs { + pub secret: std::sync::Arc>, + pub cookie_key: actix_web::cookie::Key, + pub assets: std::path::PathBuf, + pub data: std::path::PathBuf, +} diff --git a/rc_society/src/main.rs b/rc_society/src/main.rs new file mode 100644 index 0000000..1473258 --- /dev/null +++ b/rc_society/src/main.rs @@ -0,0 +1,78 @@ +#![forbid(unsafe_code)] +mod cli; +mod api; +mod web; + +use actix_web::{App, HttpServer, Responder}; + +pub static START_TIME: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1); + +#[actix_web::get("/version")] +async fn version_info() -> impl Responder { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]); + let authors = env!("CARGO_PKG_AUTHORS"); + let license = env!("CARGO_PKG_LICENSE"); + let repo = env!("CARGO_PKG_REPOSITORY"); + format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let cli_args = cli::CliArgs::get(); + + let config = oj_rc_core::ConfigImpl::load(&cli_args.assets_robocraft)?; + + let server_settings = actix_web::web::Data::new(>::server_config(&config)); + + let users = oj_rc_core::UserImpl::load(&cli_args.data_robocraft, &config).await.expect("Bad user data"); + let auth_ref = actix_web::web::Data::new(Box::new(users)); + + let cli_args2 = actix_web::web::Data::new(cli_args.clone()); + let loadeds_args = actix_web::web::Data::new(cli_args.loaded()); + + let mut handlebars_conf = handlebars::Handlebars::new(); + let mut dir_conf = handlebars::DirectorySourceOptions::default(); + dir_conf.tpl_extension = ".html.hbs".to_owned(); + dir_conf.hidden = false; + dir_conf.temporary = false; + handlebars_conf + .register_templates_directory( + std::path::PathBuf::from(&cli_args.assets_robocraft).parent().expect("Bad robocraft asset path").join("templates/rc_society"), + dir_conf, + ) + .unwrap(); + let handlebars_ref = actix_web::web::Data::new(handlebars_conf); + + START_TIME.store(chrono::Utc::now().timestamp(), std::sync::atomic::Ordering::SeqCst); + + HttpServer::new(move || { + App::new() + .wrap_fn(|req, srv| { + use actix_web::dev::Service; + log::trace!("Request {} {}", req.method(), req.path()); + srv.call(req) + }) + .wrap(actix_identity::IdentityMiddleware::default()) + .wrap(actix_session::SessionMiddleware::new( + actix_session::storage::CookieSessionStore::default(), + loadeds_args.cookie_key.clone(), + )) + .app_data(cli_args2.clone()) + .app_data(loadeds_args.clone()) + .app_data(handlebars_ref.clone()) + .app_data(server_settings.clone()) + .app_data(auth_ref.clone()) + .service(version_info) + .service(web::login::form_submit) + .service(web::login::form_load) + .service(web::favicon::favicon_standard) + .service(web::dashboard::get) + .service(web::index::get) + }) + .bind((cli_args.ip, cli_args.port))? + .run() + .await +} diff --git a/rc_society/src/web/dashboard.rs b/rc_society/src/web/dashboard.rs new file mode 100644 index 0000000..ea760e7 --- /dev/null +++ b/rc_society/src/web/dashboard.rs @@ -0,0 +1,65 @@ +use actix_web::{get, web::Data, Responder, HttpRequest}; +use actix_identity::Identity; +use serde::{Serialize, Deserialize}; + +const FORM_NAME: &str = "dashboard"; + +#[derive(Serialize, Deserialize)] +struct RenderData { + // TODO + display_name: String, + public_id: String, + debug: DebugData, + perms: PermissionData, +} + +#[derive(Serialize, Deserialize)] +struct DebugData { + user_id: i32, + creation_time_unix: i64, + creation_time_iso: String, +} + +#[derive(Serialize, Deserialize)] +struct PermissionData { + r#mod: bool, + admin: bool, + dev: bool, + royal: bool, + banned: bool, +} + +#[get("/dashboard")] +pub async fn get(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match super::try_auth_user(user_opt, auth.as_ref(), &req).await? { + super::LoginReturn::AuthFail(resp) => Ok(resp), + super::LoginReturn::Success(user) => { + // TODO + let creation_time = user.creation(); + let creation_time_chrono = chrono::DateTime::::from_timestamp_secs(creation_time).unwrap_or_default(); + Ok(super::render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + debug: DebugData { + user_id: user.account_id(), + creation_time_unix: creation_time, + creation_time_iso: creation_time_chrono.to_rfc3339(), + }, + perms: PermissionData { + r#mod: user.is_mod(), + admin: user.is_admin(), + dev: user.is_dev(), + royal: user.is_royal(), + banned: user.is_banned(), + } + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} diff --git a/rc_society/src/web/favicon.rs b/rc_society/src/web/favicon.rs new file mode 100644 index 0000000..0fef295 --- /dev/null +++ b/rc_society/src/web/favicon.rs @@ -0,0 +1,11 @@ +use actix_web::{get, web::Data, Responder}; + +async fn favicon_impl(cli_args: Data) -> impl Responder { + let path = std::path::PathBuf::from(&cli_args.assets).join("favicon.jpg"); + actix_files::NamedFile::open_async(path).await +} + +#[get("/favicon.ico")] +pub async fn favicon_standard(cli_args: Data) -> impl Responder { + favicon_impl(cli_args).await +} diff --git a/rc_society/src/web/index.rs b/rc_society/src/web/index.rs new file mode 100644 index 0000000..b829bce --- /dev/null +++ b/rc_society/src/web/index.rs @@ -0,0 +1,77 @@ +use actix_web::{get, web::Data, Responder, HttpRequest}; +use actix_identity::Identity; +use serde::{Serialize, Deserialize}; + +pub const FORM_NAME: &str = "index"; + +#[derive(Serialize, Deserialize)] +struct RenderData { + is_logged_in: bool, + display_name: Option, + server: ServerDetails, +} + +#[derive(Serialize, Deserialize)] +struct ServerDetails { + domain: String, + cdn: String, + auth: String, + min_version: i32, + server_version: String, + start_time_iso: String, + start_time_unix: i64, +} + +fn server_details(conf: &oj_rc_core::persist::config::ServerConfig) -> ServerDetails { + let start_time_unix = crate::START_TIME.load(std::sync::atomic::Ordering::Relaxed); + let start_time_chrono = chrono::DateTime::::from_timestamp_secs(start_time_unix).unwrap_or_default(); + let version = env!("CARGO_PKG_VERSION"); + let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]); + let server_version = format!("{}:{}", version, git_version); + ServerDetails { + domain: conf.domain.clone(), + cdn: conf.cdn_url.clone(), + auth: conf.auth_url.clone(), + min_version: conf.minimum_version, + server_version, + start_time_unix, + start_time_iso: start_time_chrono.to_rfc3339(), + } +} + +#[get("/")] +pub async fn get(handlebars_ref: Data>, auth: Data>, server_config: Data, user_opt: Option, req: HttpRequest) -> Result { + let server_info = server_details(server_config.as_ref()); + if let Some(user) = user_opt { + match super::try_auth_user(Some(user), auth.as_ref(), &req).await? { + super::LoginReturn::AuthFail(resp) => Ok(resp), + super::LoginReturn::Success(user) => { + Ok(super::render_ok( + RenderData { + is_logged_in: true, + display_name: Some(user.display_name().to_owned()), + server: server_info, + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + .respond_to(&req) + .map_into_boxed_body() + ) + } + } + } else { + Ok(super::render_ok( + RenderData { + is_logged_in: false, + display_name: None, + server: server_info, + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + .respond_to(&req) + .map_into_boxed_body() + ) + } +} diff --git a/rc_society/src/web/login.rs b/rc_society/src/web/login.rs new file mode 100644 index 0000000..f700dbe --- /dev/null +++ b/rc_society/src/web/login.rs @@ -0,0 +1,66 @@ +use actix_web::{get, post, web::{Data, Form, Redirect}, Responder, HttpRequest, HttpMessage}; +use actix_identity::Identity; +use serde::{Serialize, Deserialize}; + +const FORM_NAME: &str = "login"; + +#[derive(Serialize, Deserialize, Default)] +struct LoginForm { + username: String, + password: String, +} + +#[post("/login")] +pub async fn form_submit(form: Form, auth: Data>, handlebars_ref: Data>, req: HttpRequest) -> Result { + use oj_rc_core::UserAuthenticator; + let auth_result = auth.login(oj_rc_core::persist::user::UserAuthInfo::Username { + username: form.username.to_owned(), + password: form.password.to_owned(), + }).await; + match auth_result { + Ok(user) => { + let resp = Redirect::to("/dashboard") + .respond_to(&req) + .map_into_boxed_body(); + Identity::login(&req.extensions(), user.response.token)?; + Ok(resp) + }, + Err(e) => { + Ok(super::render_err(form.0, e.message, handlebars_ref.as_ref(), FORM_NAME) + .respond_to(&req) + .map_into_boxed_body()) + } + } +} + +#[get("/login")] +pub async fn form_load(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + if let Some(user) = user_opt { + let user_id = user.id()?; + use oj_rc_core::UserAuthenticator; + if auth.verify(user_id).await.is_ok() { + Ok(Redirect::to("/dashboard") + .respond_to(&req) + .map_into_boxed_body()) + } else { + Ok(super::render_err( + LoginForm::default(), + "Invalid login token".to_owned(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + .respond_to(&req) + .map_into_boxed_body() + ) + } + } else { + Ok(super::render_ok( + LoginForm::default(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + .respond_to(&req) + .map_into_boxed_body() + ) + } +} diff --git a/rc_society/src/web/mod.rs b/rc_society/src/web/mod.rs new file mode 100644 index 0000000..2422763 --- /dev/null +++ b/rc_society/src/web/mod.rs @@ -0,0 +1,71 @@ +pub mod dashboard; +pub mod login; +pub mod favicon; +pub mod index; + +use serde::Serialize; +use actix_web::{web::{Html, Redirect}, Responder}; + +fn version_string() -> String { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + //let license = env!("CARGO_PKG_LICENSE"); + //let repo = env!("CARGO_PKG_REPOSITORY"); + format!("OpenJam {} {}", name, version) +} + +#[derive(Serialize)] +struct Context { + form: T, + error: Option, + version: String, + source_url: String, +} + +fn render_ok(form: T, renderer: &handlebars::Handlebars<'_>, form_name: &str) -> Html { + let rendered = renderer.render(form_name, &Context { + form, + error: None, + version: version_string(), + source_url: env!("CARGO_PKG_REPOSITORY").to_owned(), + }).unwrap(); + Html::new(rendered) +} + +fn render_err(form: T, error: String , renderer: &handlebars::Handlebars<'_>, form_name: &str) -> Html { + let rendered = renderer.render(form_name, &Context { + form, + error: Some(error), + version: version_string(), + source_url: env!("CARGO_PKG_REPOSITORY").to_owned(), + }).unwrap(); + Html::new(rendered) +} + +enum LoginReturn { + Success(Box), + AuthFail(actix_web::HttpResponse), +} + +async fn try_auth_user(user_opt: Option, auth: &oj_rc_core::UserImpl, req: &actix_web::HttpRequest) -> Result { + if let Some(user) = user_opt { + let user_id = user.id()?; + match >::web_authenticate(auth, user_id.clone()).await { + Ok(user) => Ok(LoginReturn::Success(user)), + Err(e) => { + log::warn!("Failed to login with token {}: {} ({:?})", user_id, e.message, e.code); + Ok(LoginReturn::AuthFail( + Redirect::to("/login") + .respond_to(req) + .map_into_boxed_body() + )) + } + } + } else { + Ok(LoginReturn::AuthFail( + Redirect::to("/login") + .respond_to(req) + .map_into_boxed_body() + )) + } +}