Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/database/wrappers/account_links/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,44 @@ impl AccountLinkUserId<DiscordMarker> {
.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<AccountLink> {
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::<Text, _>(&self.id)
.get_results::<AccountLink>(conn)
.unwrap_or_default()
}

/// Delete all account links associated with the **Discord ID**.
///
/// # Arguments
Expand Down
2 changes: 1 addition & 1 deletion src/database/wrappers/account_links/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
80 changes: 45 additions & 35 deletions src/users/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,50 +15,60 @@ pub(crate) fn routes() -> Vec<Route> {
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<Json<UserInfoResponse>> {
let primary_account_link = conn
.run(move |conn| {
let discord_marker = AccountLinkUserId::<DiscordMarker>::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<Json<UserInfoResponse>> {
// Account links associated with the Discord ID
let account_links = conn
.run(move |conn| {
let discord_marker = AccountLinkUserId::<DiscordMarker>::new(discord_uid);

discord_marker.find_many(conn)
AccountLinkUserId::<DiscordMarker>::new(session.discord_uid).find_associated(conn)
})
.await;

let mut discord_links: Vec<String> = vec![];
let mut roblox_links: Vec<String> = 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<User> = 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,
}))
}
32 changes: 20 additions & 12 deletions src/users/types.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub(crate) roblox: Vec<String>,
pub(super) struct UserLinkedAccounts {
/// All Discord IDs associated with the user.
pub(super) discord: HashSet<String>,
/// All Roblox IDs associated with the user.
pub(super) roblox: HashSet<String>,
}
Loading