feat: more details linked account info for support/moderation (#6897)

* feat: show discord + steam ID + github profile in "Show details" user modal

* pnpm intl:extract
This commit is contained in:
François-Xavier Talbot
2026-07-27 19:20:38 +00:00
committed by GitHub
parent e192f6a0af
commit 224c28f29d
6 changed files with 139 additions and 4 deletions
@@ -3374,6 +3374,15 @@
"profile.collection.projects-count": { "profile.collection.projects-count": {
"message": "{count, plural, one {# project} other {# projects}}" "message": "{count, plural, one {# project} other {# projects}}"
}, },
"profile.details.error.github-popup-blocked": {
"message": "Allow pop-ups for Modrinth, then try again."
},
"profile.details.error.github-profile-message": {
"message": "The GitHub profile could not be retrieved. Please try again."
},
"profile.details.error.github-profile-title": {
"message": "Unable to open GitHub profile"
},
"profile.details.label.auth-providers": { "profile.details.label.auth-providers": {
"message": "Auth providers" "message": "Auth providers"
}, },
@@ -3389,9 +3398,15 @@
"profile.details.label.has-totp": { "profile.details.label.has-totp": {
"message": "Has TOTP" "message": "Has TOTP"
}, },
"profile.details.label.loading-github-profile": {
"message": "Loading..."
},
"profile.details.label.payment-methods": { "profile.details.label.payment-methods": {
"message": "Payment methods" "message": "Payment methods"
}, },
"profile.details.label.view-github-profile": {
"message": "View profile"
},
"profile.details.tooltip.email-not-verified": { "profile.details.tooltip.email-not-verified": {
"message": "Email not verified" "message": "Email not verified"
}, },
+105 -1
View File
@@ -71,7 +71,37 @@
<span class="text-lg font-bold text-primary">{{ <span class="text-lg font-bold text-primary">{{
formatMessage(messages.authProvidersLabel) formatMessage(messages.authProvidersLabel)
}}</span> }}</span>
<span>{{ user.auth_providers.join(', ') }}</span> <div class="flex flex-col">
<span
v-for="provider in user.auth_providers ?? []"
:key="provider"
class="flex items-center"
>
<span>{{ authProviderNames[provider] ?? provider }}</span>
<span v-if="provider === 'discord' && user.discord_id" class="ml-1">
({{ user.discord_id }})
</span>
<template v-else-if="provider === 'github' && user.github_id">
<span class="ml-1">(</span>
<button
type="button"
class="m-0 appearance-none border-0 bg-transparent p-0 font-[inherit] text-link disabled:cursor-wait disabled:opacity-70"
:disabled="isLoadingGithubProfile"
@click="openGithubProfile"
>
{{
isLoadingGithubProfile
? formatMessage(messages.loadingGithubProfileLabel)
: formatMessage(messages.viewGithubProfileLabel)
}}
</button>
<span>)</span>
</template>
<span v-else-if="provider === 'steam' && user.steam_id" class="ml-1">
({{ user.steam_id }})
</span>
</span>
</div>
</div> </div>
<div v-if="isAdmin(auth.user)" class="flex flex-col gap-1"> <div v-if="isAdmin(auth.user)" class="flex flex-col gap-1">
@@ -437,6 +467,26 @@ const messages = defineMessages({
id: 'profile.details.label.auth-providers', id: 'profile.details.label.auth-providers',
defaultMessage: 'Auth providers', defaultMessage: 'Auth providers',
}, },
viewGithubProfileLabel: {
id: 'profile.details.label.view-github-profile',
defaultMessage: 'View profile',
},
loadingGithubProfileLabel: {
id: 'profile.details.label.loading-github-profile',
defaultMessage: 'Loading...',
},
githubProfileErrorTitle: {
id: 'profile.details.error.github-profile-title',
defaultMessage: 'Unable to open GitHub profile',
},
githubProfileErrorMessage: {
id: 'profile.details.error.github-profile-message',
defaultMessage: 'The GitHub profile could not be retrieved. Please try again.',
},
githubPopupBlockedMessage: {
id: 'profile.details.error.github-popup-blocked',
defaultMessage: 'Allow pop-ups for Modrinth, then try again.',
},
paymentMethodsLabel: { paymentMethodsLabel: {
id: 'profile.details.label.payment-methods', id: 'profile.details.label.payment-methods',
defaultMessage: 'Payment methods', defaultMessage: 'Payment methods',
@@ -518,6 +568,16 @@ const messages = defineMessages({
const client = injectModrinthClient() const client = injectModrinthClient()
const userId = useRouteId('user') const userId = useRouteId('user')
const authProviderNames = {
github: 'GitHub',
discord: 'Discord',
microsoft: 'Microsoft',
gitlab: 'GitLab',
google: 'Google',
steam: 'Steam',
paypal: 'PayPal',
}
const isLoadingGithubProfile = ref(false)
const { const {
data: user, data: user,
@@ -664,6 +724,50 @@ async function copyPermalink() {
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`) await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
} }
async function openGithubProfile() {
const githubId = user.value?.github_id
if (!githubId || isLoadingGithubProfile.value) return
const profileWindow = window.open('about:blank', '_blank')
if (!profileWindow) {
addNotification({
type: 'error',
title: formatMessage(messages.githubProfileErrorTitle),
message: formatMessage(messages.githubPopupBlockedMessage),
})
return
}
profileWindow.opener = null
isLoadingGithubProfile.value = true
try {
const githubUser = await client.request(`/${githubId}`, {
api: 'https://api.github.com',
version: 'user',
method: 'GET',
headers: { 'Content-Type': '' },
skipAuth: true,
})
if (!githubUser?.login) {
throw new Error('GitHub user response did not include a login')
}
profileWindow.location.replace(`https://github.com/${encodeURIComponent(githubUser.login)}`)
} catch (error) {
profileWindow.close()
console.error('Failed to retrieve GitHub profile:', error)
addNotification({
type: 'error',
title: formatMessage(messages.githubProfileErrorTitle),
message: formatMessage(messages.githubProfileErrorMessage),
})
} finally {
isLoadingGithubProfile.value = false
}
}
function reportProfileFromHeader() { function reportProfileFromHeader() {
if (!user.value) return if (!user.value) return
if (auth.value.user) { if (auth.value.user) {
-1
View File
@@ -24,7 +24,6 @@ pub struct LegacyUser {
pub has_totp: Option<bool>, pub has_totp: Option<bool>,
pub payout_data: Option<UserPayoutData>, // this was changed in v3, but not ones we want to keep out of v2 pub payout_data: Option<UserPayoutData>, // this was changed in v3, but not ones we want to keep out of v2
// DEPRECATED. Always returns None
pub github_id: Option<u64>, pub github_id: Option<u64>,
} }
+8 -1
View File
@@ -70,8 +70,11 @@ pub struct User {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub moderation_notes: Option<Option<ModerationNote>>, pub moderation_notes: Option<Option<ModerationNote>>,
// DEPRECATED. Always returns None
pub github_id: Option<u64>, pub github_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub steam_id: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -119,6 +122,8 @@ impl From<DBUser> for User {
has_password: None, has_password: None,
has_totp: None, has_totp: None,
github_id: None, github_id: None,
discord_id: None,
steam_id: None,
stripe_customer_id: None, stripe_customer_id: None,
allow_friend_requests: None, allow_friend_requests: None,
eligibility_verified_at: None, eligibility_verified_at: None,
@@ -180,6 +185,8 @@ impl User {
has_password: Some(db_user.password.is_some()), has_password: Some(db_user.password.is_some()),
has_totp: Some(db_user.totp_secret.is_some()), has_totp: Some(db_user.totp_secret.is_some()),
github_id: None, github_id: None,
discord_id: None,
steam_id: None,
payout_data: Some(UserPayoutData { payout_data: Some(UserPayoutData {
paypal_address: db_user.paypal_email, paypal_address: db_user.paypal_email,
paypal_country: db_user.paypal_country, paypal_country: db_user.paypal_country,
+9 -1
View File
@@ -481,7 +481,15 @@ pub async fn user_get(
let user_id = data.id; let user_id = data.id;
let mut response: crate::models::users::User = if is_admin { let mut response: crate::models::users::User = if is_admin {
crate::models::users::User::from_full(data) let github_id =
data.github_id.and_then(|id| u64::try_from(id).ok());
let discord_id = data.discord_id.map(|id| id.to_string());
let steam_id = data.steam_id.map(|id| id.to_string());
let mut user = crate::models::users::User::from_full(data);
user.github_id = github_id;
user.discord_id = discord_id;
user.steam_id = steam_id;
user
} else { } else {
data.into() data.into()
}; };
@@ -1663,6 +1663,8 @@ export namespace Labrinth {
allow_friend_requests?: boolean allow_friend_requests?: boolean
moderation_notes?: Common.ModerationNote | null moderation_notes?: Common.ModerationNote | null
github_id?: number github_id?: number
discord_id?: string
steam_id?: string
} }
export type SearchUser = { export type SearchUser = {