diff --git a/src/database/wrappers/account_links/mod.rs b/src/database/wrappers/account_links/mod.rs index 3d8ee71..9e4d54f 100644 --- a/src/database/wrappers/account_links/mod.rs +++ b/src/database/wrappers/account_links/mod.rs @@ -225,6 +225,44 @@ 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) + .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..d238517 100644 --- a/src/users/mod.rs +++ b/src/users/mod.rs @@ -15,50 +15,60 @@ 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")] -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..257c774 100644 --- a/src/users/types.rs +++ b/src/users/types.rs @@ -1,22 +1,30 @@ use rocket::serde::Serialize; +use std::collections::HashSet; -#[derive(Serialize)] +/// 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, } -#[derive(Serialize)] +/// 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, } -#[derive(Serialize)] +/// The user's account links (verification history). +#[derive(Serialize, Default, Debug)] #[serde(crate = "rocket::serde")] -pub(crate) struct UserLinkedAccounts { - pub(crate) discord: Vec, - pub(crate) roblox: Vec, +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, }