From d3bd795a01db77b3387c4f847eb4d9419e9396d7 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 11 May 2025 21:18:45 -0400 Subject: [PATCH] Add real email-based authentication, fix RC account auth to try all 3 login options when it doesn't know #20 --- auth/src/robocraft/email.rs | 31 +++++++++++++++++++++++ auth/src/robocraft/mod.rs | 2 ++ auth/src/robocraft/username.rs | 8 +++--- rc_core/src/persist/user/account_json.rs | 14 ++++++++--- rc_core/src/persist/user/initial_data.rs | 2 +- rc_core/src/persist/user/traits.rs | 5 +++- rc_database/src/wrapper.rs | 32 ++++++++++++++++++++++-- 7 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 auth/src/robocraft/email.rs diff --git a/auth/src/robocraft/email.rs b/auth/src/robocraft/email.rs new file mode 100644 index 0000000..4ba0ee5 --- /dev/null +++ b/auth/src/robocraft/email.rs @@ -0,0 +1,31 @@ +use rc_core::UserAuthenticator; +use rocket::{post, routes, serde::json::Json, http::Status, State}; + +#[post("/authenticate/email/game", data = "")] +pub async fn email_password_auth(body: Json, config: &State) -> Result, Status> { + log::info!("Authenticating {} user {}", body.target, body.display_name); + let payload = libfj::robocraft::TokenPayload { + public_id: body.display_name.clone(), + display_name: body.display_name.clone(), + robocraft_name: body.display_name.clone(), + email_address: body.email_address.clone(), + email_verified: true, + flags: Vec::new(), + }; + let user_info = rc_core::persist::user::UserInfo { + payload, + extra: rc_core::persist::user::ExtraUserInfo::Email { password: body.password.clone() }, + }; + let response = config.robocraft.account_provider.login(user_info).await + .map_err(|e| { + log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e); + Status { code: 401 } + })?; + Ok(Json(response.response)) +} + +pub fn stage() -> rocket::fairing::AdHoc { + rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async { + rocket.mount("/", routes![email_password_auth]) + }) +} diff --git a/auth/src/robocraft/mod.rs b/auth/src/robocraft/mod.rs index 8abf41d..43316f9 100644 --- a/auth/src/robocraft/mod.rs +++ b/auth/src/robocraft/mod.rs @@ -1,10 +1,12 @@ mod debug; mod username; mod steam; +mod email; pub fn stage() -> rocket::fairing::AdHoc { rocket::fairing::AdHoc::on_ignite("robocraft", |rocket| async { rocket.attach(username::stage()) + .attach(email::stage()) .attach(steam::stage()) .attach(debug::stage()) .register("/", rocket::catchers![unauthorized]) diff --git a/auth/src/robocraft/username.rs b/auth/src/robocraft/username.rs index 94f8ee1..354d32a 100644 --- a/auth/src/robocraft/username.rs +++ b/auth/src/robocraft/username.rs @@ -2,7 +2,7 @@ use rc_core::UserAuthenticator; use rocket::{post, routes, serde::json::Json, http::Status, State}; #[post("/authenticate/robocraft/game", data = "")] -pub async fn email_password_auth(body: Json, config: &State) -> Result, Status> { +pub async fn user_password_auth(body: Json, config: &State) -> Result, Status> { log::info!("Authenticating {} user {}", body.target, body.display_name); let payload = libfj::robocraft::TokenPayload { public_id: body.display_name.clone(), @@ -14,7 +14,7 @@ pub async fn email_password_auth(body: Json rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("Robocraft Email/Password", |rocket| async { - rocket.mount("/", routes![email_password_auth]) + rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async { + rocket.mount("/", routes![user_password_auth]) }) } diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 9f502f8..4993439 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -33,7 +33,7 @@ impl super::UserProvider for AccountProvider { let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); validation.set_required_spec_claims::<&str>(&[]); jsonwebtoken::decode::(&token.token, &secret, &validation).map_err(|e| e.to_string())?; - let user_info = if let Some(user_info) = self.db.user_by_public_id(token.uuid.clone()).await.map_err(|e| e.to_string())? { + let user_info = if let Some(user_info) = self.db.user_by_any_unique_id(token.uuid.clone()).await.map_err(|e| e.to_string())? { user_info } else { return Err("User not found".to_owned()); @@ -62,14 +62,19 @@ impl super::UserAuthenticator for AccountProvider { async fn login(&self, info: super::UserInfo) -> Result { //let new_root = self.root.join(&info.payload.public_id); let is_new_user; - let mut user_info = if let Some(user_info) = self.db.user_by_public_id(info.payload.public_id.clone()).await.map_err(|e| e.to_string())? { + let user_opt = match &info.extra { + super::ExtraUserInfo::Steam { id } => self.db.user_by_steam_id(*id).await, + super::ExtraUserInfo::Email { .. } => self.db.user_by_email(info.payload.email_address.clone()).await, + super::ExtraUserInfo::Username { .. } => self.db.user_by_display_name(info.payload.display_name.clone()).await, + }.map_err(|e| e.to_string())?; + let mut user_info = if let Some(user_info) = user_opt { is_new_user = false; user_info } else { is_new_user = true; log::info!("New user {}", info.payload.public_id); super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?; - self.db.user_by_public_id(info.payload.public_id.clone()).await.map_err(|e| e.to_string())?.unwrap() + self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| e.to_string())?.unwrap() }; let override_password = user_info.password.is_empty() && user_info.steam_id.is_none(); match info.extra { @@ -83,7 +88,8 @@ impl super::UserAuthenticator for AccountProvider { return Err("SteamID not supported for this user".to_owned()); } }, - super::ExtraUserInfo::Standalone { password } => { + super::ExtraUserInfo::Email { password } + | super::ExtraUserInfo::Username { password } => { use argon2::password_hash::PasswordHasher; let argon2_algo = argon2::Argon2::default(); if override_password { diff --git a/rc_core/src/persist/user/initial_data.rs b/rc_core/src/persist/user/initial_data.rs index 45e1076..74c2651 100644 --- a/rc_core/src/persist/user/initial_data.rs +++ b/rc_core/src/persist/user/initial_data.rs @@ -32,7 +32,7 @@ pub async fn setup_new_user(user: &super::UserInfo, db: &rc_database::Database) } fn default_user_data(user: &super::UserInfo) -> rc_database::schema::user::ActiveModel { - let password = if let super::ExtraUserInfo::Standalone { password } = &user.extra { + let password = if let super::ExtraUserInfo::Email { password } | super::ExtraUserInfo::Username { password } = &user.extra { //password.to_owned() use argon2::password_hash::PasswordHasher; let argon2_algo = argon2::Argon2::default(); diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 0a6ff4f..28e100c 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -15,7 +15,10 @@ pub enum ExtraUserInfo { Steam { id: u64, }, - Standalone { + Username { + password: String, + }, + Email { password: String, } } diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index b710e99..7f5b309 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -15,13 +15,41 @@ impl Database { }) } - pub async fn user_by_public_id(&self, public_id: String) -> Result, sea_orm::DbErr> { + pub async fn user_by_display_name(&self, public_id: String) -> Result, sea_orm::DbErr> { crate::schema::user::Entity::find() - .filter(crate::schema::user::Column::PublicId.eq(public_id)) + .filter(crate::schema::user::Column::DisplayName.eq(public_id)) .one(&self.orm) .await } + pub async fn user_by_steam_id(&self, steam_id: u64) -> Result, sea_orm::DbErr> { + crate::schema::user::Entity::find() + .filter(crate::schema::user::Column::SteamId.eq(Some(steam_id))) + .one(&self.orm) + .await + } + + pub async fn user_by_email(&self, email: String) -> Result, sea_orm::DbErr> { + crate::schema::user::Entity::find() + .filter(crate::schema::user::Column::Email.eq(email)) + .one(&self.orm) + .await + } + + pub async fn user_by_any_unique_id(&self, id: String) -> Result, sea_orm::DbErr> { + if let Ok(steam_id) = id.parse::() { + if let Some(res) = self.user_by_steam_id(steam_id).await? { + return Ok(Some(res)); + } + } + if id.contains('@') { + if let Some(res) = self.user_by_email(id.clone()).await? { + return Ok(Some(res)); + } + } + self.user_by_display_name(id.clone()).await + } + pub async fn insert_user(&self, entity: crate::schema::user::ActiveModel) -> Result { entity.insert(&self.orm).await }