From ad8bdf6e133626c8ffce4954be0e9de303dddd8e Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:55:39 +0000 Subject: [PATCH 1/3] fix(user-api): Return correct account history --- src/database/wrappers/account_links/mod.rs | 41 +++++++++++ src/database/wrappers/account_links/models.rs | 2 +- src/users/mod.rs | 70 +++++++++---------- src/users/types.rs | 11 +-- 4 files changed, 83 insertions(+), 41 deletions(-) diff --git a/src/database/wrappers/account_links/mod.rs b/src/database/wrappers/account_links/mod.rs index 3d8ee71..3bb2f97 100644 --- a/src/database/wrappers/account_links/mod.rs +++ b/src/database/wrappers/account_links/mod.rs @@ -225,6 +225,47 @@ impl AccountLinkUserId { .unwrap_or_default() } + /// Find all account links associated with the **Discord ID** recursively. + /// + /// # Arguments + /// + /// * `conn` - The database connection. + /// + /// # Returns + /// + /// A vector of [`AccountLink`]s associated with the **Discord ID**. + pub(crate) fn find_associated(&self, conn: &mut PgConnection) -> Vec { + use diesel::dsl::sql_query; + use diesel::sql_types::Text; + + sql_query( + " + WITH RECURSIVE linked_accounts AS ( + -- Base case: Start with all rows associated with the initial discord_uid + SELECT * + FROM account_links + WHERE discord_uid = $1 + + UNION + + -- Recursive step: Find rows associated with the current roblox_uid or discord_uid + SELECT al.* + FROM account_links al + INNER JOIN linked_accounts la + ON al.discord_uid = la.discord_uid OR al.roblox_uid = la.roblox_uid + ) + SELECT DISTINCT * + FROM linked_accounts la; + ", + ) + .bind::(&self.id) + .get_results::(conn) + .inspect_err(|e| { + rocket::error!("Failed to find associated account links: {:?}", e); + }) + .unwrap_or_default() + } + /// Delete all account links associated with the **Discord ID**. /// /// # Arguments diff --git a/src/database/wrappers/account_links/models.rs b/src/database/wrappers/account_links/models.rs index f0e5043..2bc1cab 100644 --- a/src/database/wrappers/account_links/models.rs +++ b/src/database/wrappers/account_links/models.rs @@ -2,7 +2,7 @@ use super::schema; use diesel::prelude::*; /// Represents a link between a Discord account and a Roblox account. -#[derive(Queryable, Selectable, Debug)] +#[derive(Queryable, QueryableByName, Selectable, Debug, Clone)] #[diesel(table_name = schema::account_links)] #[diesel(check_for_backend(diesel::pg::Pg))] pub(crate) struct AccountLink { diff --git a/src/users/mod.rs b/src/users/mod.rs index 4780aa1..af8c9bc 100644 --- a/src/users/mod.rs +++ b/src/users/mod.rs @@ -16,49 +16,49 @@ pub(crate) fn routes() -> Vec { } #[get("/@me")] -pub(super) async fn me(conn: DbConn, session: Session) -> ApiResult> { - let primary_account_link = conn - .run(move |conn| { - let discord_marker = AccountLinkUserId::::new(session.discord_uid); - - discord_marker.find_primary(conn) - }) - .await - .ok_or_else(|| { - ApiError::message( - Status::InternalServerError, - "User doesn't have a primary account link", - ) - })?; - - let discord_uid = primary_account_link.discord_uid.clone(); +async fn me(conn: DbConn, session: Session) -> ApiResult> { + // Account links associated with the Discord ID let account_links = conn .run(move |conn| { - let discord_marker = AccountLinkUserId::::new(discord_uid); - - discord_marker.find_many(conn) + AccountLinkUserId::::new(session.discord_uid).find_associated(conn) }) .await; - let mut discord_links: Vec = vec![]; - let mut roblox_links: Vec = vec![]; - - for account_link in account_links { - discord_links.push(account_link.discord_uid); - roblox_links.push(account_link.roblox_uid); + // No data can be returned if the user has no account links + if account_links.is_empty() { + return Err(ApiError::message( + Status::NotFound, + "No account links found for this user", + )); } - let user_info = UserInfoResponse { - user: User { - discord_uid: primary_account_link.discord_uid, - roblox_uid: primary_account_link.roblox_uid, - }, + let mut user: Option = None; + let mut linked_accounts = UserLinkedAccounts::default(); + + // Populate the linked accounts with the Discord and Roblox IDs + for link in account_links { + if link.is_primary { + user = Some(User { + discord_uid: link.discord_uid.clone(), + roblox_uid: link.roblox_uid.clone(), + }); + } + + linked_accounts.discord.insert(link.discord_uid); + linked_accounts.roblox.insert(link.roblox_uid); + } - linked_accounts: UserLinkedAccounts { - discord: discord_links, - roblox: roblox_links, - }, + // If the user has no primary account link, return 500 + // This should never happen if the user has account links + let Some(user) = user else { + return Err(ApiError::message( + Status::InternalServerError, + "No primary account link found for this user", + )); }; - Ok(Json(user_info)) + Ok(Json(UserInfoResponse { + user, + linked_accounts, + })) } diff --git a/src/users/types.rs b/src/users/types.rs index 30bad32..de263db 100644 --- a/src/users/types.rs +++ b/src/users/types.rs @@ -1,22 +1,23 @@ use rocket::serde::Serialize; +use std::collections::HashSet; -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[serde(crate = "rocket::serde")] pub(crate) struct UserInfoResponse { pub(crate) user: User, pub(crate) linked_accounts: UserLinkedAccounts, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[serde(crate = "rocket::serde")] pub(crate) struct User { pub(crate) discord_uid: String, pub(crate) roblox_uid: String, } -#[derive(Serialize)] +#[derive(Serialize, Default, Debug)] #[serde(crate = "rocket::serde")] pub(crate) struct UserLinkedAccounts { - pub(crate) discord: Vec, - pub(crate) roblox: Vec, + pub(crate) discord: HashSet, + pub(crate) roblox: HashSet, } From 364ee7ef8a1356e1010de7e7e3bfd8d3cd5a9b87 Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:59:56 +0000 Subject: [PATCH 2/3] docs(user-api): Document additions --- src/users/mod.rs | 10 ++++++++++ src/users/types.rs | 25 ++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/users/mod.rs b/src/users/mod.rs index af8c9bc..d238517 100644 --- a/src/users/mod.rs +++ b/src/users/mod.rs @@ -15,6 +15,16 @@ pub(crate) fn routes() -> Vec { routes![me] } +/// Get the authorized user's information +/// +/// # Possible Responses +/// +/// - `200 OK` with a serialized [`UserInfoResponse`] struct. +/// - `401 Unauthorized` +/// - If the session cookie is missing. +/// - If the session is not found in the database. +/// - `404 Not Found` if the user has no account links. +/// - `500 Internal Server Error` if the user has no primary account link. #[get("/@me")] async fn me(conn: DbConn, session: Session) -> ApiResult> { // Account links associated with the Discord ID diff --git a/src/users/types.rs b/src/users/types.rs index de263db..257c774 100644 --- a/src/users/types.rs +++ b/src/users/types.rs @@ -1,23 +1,30 @@ use rocket::serde::Serialize; use std::collections::HashSet; +/// The response for the `GET /@me` route. #[derive(Serialize, Debug)] #[serde(crate = "rocket::serde")] -pub(crate) struct UserInfoResponse { - pub(crate) user: User, - pub(crate) linked_accounts: UserLinkedAccounts, +pub(super) struct UserInfoResponse { + pub(super) user: User, + pub(super) linked_accounts: UserLinkedAccounts, } +/// The user's primary account link. #[derive(Serialize, Debug)] #[serde(crate = "rocket::serde")] -pub(crate) struct User { - pub(crate) discord_uid: String, - pub(crate) roblox_uid: String, +pub(super) struct User { + /// The user's Discord ID. + pub(super) discord_uid: String, + /// The user's Roblox ID. + pub(super) roblox_uid: String, } +/// The user's account links (verification history). #[derive(Serialize, Default, Debug)] #[serde(crate = "rocket::serde")] -pub(crate) struct UserLinkedAccounts { - pub(crate) discord: HashSet, - pub(crate) roblox: HashSet, +pub(super) struct UserLinkedAccounts { + /// All Discord IDs associated with the user. + pub(super) discord: HashSet, + /// All Roblox IDs associated with the user. + pub(super) roblox: HashSet, } From 8d76a5476640a7dc90eab1ef0f602b6b39b10b85 Mon Sep 17 00:00:00 2001 From: nick <59822256+Archasion@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:03:05 +0000 Subject: [PATCH 3/3] refactor(user-api): Remove debug statement (oops) --- src/database/wrappers/account_links/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/database/wrappers/account_links/mod.rs b/src/database/wrappers/account_links/mod.rs index 3bb2f97..9e4d54f 100644 --- a/src/database/wrappers/account_links/mod.rs +++ b/src/database/wrappers/account_links/mod.rs @@ -260,9 +260,6 @@ impl AccountLinkUserId { ) .bind::(&self.id) .get_results::(conn) - .inspect_err(|e| { - rocket::error!("Failed to find associated account links: {:?}", e); - }) .unwrap_or_default() }