You've already forked AstralRinth
feat: use discord_id from discord sso for role grant (#6326)
* feat: properly use labrinth's linked discord accts sso for discord bot * Update email for fixture user in SQL insert Signed-off-by: Calum H. <calum@modrinth.com> * fix: rev changes * fix: copy on email * fix: lint * fix: rev * fix: lint --------- Signed-off-by: Calum H. <calum@modrinth.com>
This commit is contained in:
@@ -1466,6 +1466,24 @@
|
||||
"dashboard.creator-withdraw-modal.withdraw-limit-used": {
|
||||
"message": "You've used up your <b>{withdrawLimit}</b> withdrawal limit. You must complete a tax form to withdraw more."
|
||||
},
|
||||
"dashboard.discord-roles.banner.body": {
|
||||
"message": "You're eligible for {roles}. Link your Discord account through Modrinth and we'll sync them automatically."
|
||||
},
|
||||
"dashboard.discord-roles.banner.cta": {
|
||||
"message": "Link Discord"
|
||||
},
|
||||
"dashboard.discord-roles.banner.title": {
|
||||
"message": "Claim your Discord roles"
|
||||
},
|
||||
"dashboard.discord-roles.role.big-creator": {
|
||||
"message": "1M+ Downloads"
|
||||
},
|
||||
"dashboard.discord-roles.role.creator": {
|
||||
"message": "Creator"
|
||||
},
|
||||
"dashboard.discord-roles.role.pride": {
|
||||
"message": "Pride 2026"
|
||||
},
|
||||
"dashboard.head-title": {
|
||||
"message": "Dashboard"
|
||||
},
|
||||
|
||||
@@ -48,28 +48,69 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="normal-page__content mt-4 lg:!mt-0">
|
||||
<Admonition
|
||||
v-if="showDiscordRoleBanner"
|
||||
class="mb-3"
|
||||
type="info"
|
||||
:header="formatMessage(messages.discordRoleBannerTitle)"
|
||||
show-actions-underneath
|
||||
dismissible
|
||||
@dismiss="dismissDiscordRoleBanner"
|
||||
>
|
||||
<div class="text-primary">
|
||||
{{
|
||||
formatMessage(messages.discordRoleBannerBody, {
|
||||
roles: eligibleDiscordRolesLabel,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<template #actions>
|
||||
<ButtonStyled color="blue">
|
||||
<NuxtLink to="/discord/link" class="w-fit !px-4">
|
||||
<ExternalIcon />
|
||||
{{ formatMessage(messages.discordRoleBannerCta) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</Admonition>
|
||||
<NuxtPage :route="route" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
AffiliateIcon,
|
||||
BellIcon as NotificationsIcon,
|
||||
ChartIcon,
|
||||
CurrencyIcon,
|
||||
DashboardIcon,
|
||||
ExternalIcon,
|
||||
LibraryIcon,
|
||||
ListIcon,
|
||||
OrganizationIcon,
|
||||
ReportIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { type User, UserBadge } from '@modrinth/utils'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { UserBadge } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
import NavStack from '~/components/ui/NavStack.vue'
|
||||
|
||||
const auth = (await useAuth()) as Ref<{ user: User | null }>
|
||||
const auth = (await useAuth()) as Ref<{ user: Labrinth.Users.v3.User | null }>
|
||||
const client = injectModrinthClient()
|
||||
const dismissedDiscordRoleBannerUsers = useLocalStorage<string[]>(
|
||||
'dashboard-discord-role-banner-dismissed-users',
|
||||
[],
|
||||
)
|
||||
|
||||
const isAffiliate = computed(() => {
|
||||
return auth.value.user && auth.value.user.badges & UserBadge.AFFILIATE
|
||||
@@ -114,6 +155,31 @@ const messages = defineMessages({
|
||||
id: 'dashboard.sidebar.label.revenue',
|
||||
defaultMessage: 'Revenue',
|
||||
},
|
||||
discordRoleBannerTitle: {
|
||||
id: 'dashboard.discord-roles.banner.title',
|
||||
defaultMessage: 'Claim your Discord roles',
|
||||
},
|
||||
discordRoleBannerBody: {
|
||||
id: 'dashboard.discord-roles.banner.body',
|
||||
defaultMessage:
|
||||
"You're eligible for {roles}. Link your Discord account through Modrinth and we'll sync them automatically.",
|
||||
},
|
||||
discordRoleBannerCta: {
|
||||
id: 'dashboard.discord-roles.banner.cta',
|
||||
defaultMessage: 'Link Discord',
|
||||
},
|
||||
discordRolePride: {
|
||||
id: 'dashboard.discord-roles.role.pride',
|
||||
defaultMessage: 'Pride 2026',
|
||||
},
|
||||
discordRoleCreator: {
|
||||
id: 'dashboard.discord-roles.role.creator',
|
||||
defaultMessage: 'Creator',
|
||||
},
|
||||
discordRoleBigCreator: {
|
||||
id: 'dashboard.discord-roles.role.big-creator',
|
||||
defaultMessage: '1M+ Downloads',
|
||||
},
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
@@ -125,4 +191,60 @@ useSeoMeta({
|
||||
})
|
||||
|
||||
const route = useNativeRoute()
|
||||
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: computed(() => ['dashboard-discord-role-eligibility', auth.value.user?.id, 'projects']),
|
||||
queryFn: () => {
|
||||
const userId = auth.value.user?.id
|
||||
if (!userId) return []
|
||||
|
||||
return client.labrinth.users_v2.getProjects(userId)
|
||||
},
|
||||
enabled: computed(() => !!auth.value.user?.id),
|
||||
})
|
||||
|
||||
const totalProjectDownloads = computed(() =>
|
||||
(projects.value ?? []).reduce((total, project) => total + (project.downloads ?? 0), 0),
|
||||
)
|
||||
|
||||
const eligibleDiscordRoles = computed(() => {
|
||||
const roles = []
|
||||
|
||||
if (auth.value.user?.campaigns?.pride_26?.has_badge === true) {
|
||||
roles.push(formatMessage(messages.discordRolePride))
|
||||
}
|
||||
|
||||
if (totalProjectDownloads.value >= 20_000) {
|
||||
roles.push(formatMessage(messages.discordRoleCreator))
|
||||
}
|
||||
|
||||
if (totalProjectDownloads.value >= 1_000_000) {
|
||||
roles.push(formatMessage(messages.discordRoleBigCreator))
|
||||
}
|
||||
|
||||
return roles
|
||||
})
|
||||
|
||||
const roleListFormatter = new Intl.ListFormat(undefined, {
|
||||
style: 'long',
|
||||
type: 'conjunction',
|
||||
})
|
||||
|
||||
const eligibleDiscordRolesLabel = computed(() =>
|
||||
roleListFormatter.format(eligibleDiscordRoles.value),
|
||||
)
|
||||
|
||||
const hasDismissedDiscordRoleBanner = computed(() =>
|
||||
dismissedDiscordRoleBannerUsers.value.includes(auth.value.user?.id ?? ''),
|
||||
)
|
||||
const showDiscordRoleBanner = computed(
|
||||
() => eligibleDiscordRoles.value.length > 0 && !hasDismissedDiscordRoleBanner.value,
|
||||
)
|
||||
|
||||
function dismissDiscordRoleBanner() {
|
||||
const userId = auth.value.user?.id
|
||||
if (!userId || dismissedDiscordRoleBannerUsers.value.includes(userId)) return
|
||||
|
||||
dismissedDiscordRoleBannerUsers.value = [...dismissedDiscordRoleBannerUsers.value, userId]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { injectModrinthClient } from '@modrinth/ui'
|
||||
|
||||
import { getAuthUrl } from '~/composables/auth.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'empty',
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const auth = await useAuth()
|
||||
const client = injectModrinthClient()
|
||||
const error = ref<unknown>(null)
|
||||
const isLinkedCallback = computed(() => route.query.callback === 'linked')
|
||||
|
||||
onMounted(async () => {
|
||||
if (isLinkedCallback.value) return
|
||||
|
||||
try {
|
||||
if (!auth.value.user?.auth_providers?.includes('discord')) {
|
||||
window.location.href = `${getAuthUrl('discord', '/discord/link')}&token=${auth.value.token}`
|
||||
return
|
||||
}
|
||||
|
||||
const res = await client.labrinth.auth_internal.createDiscordCommunityLink()
|
||||
window.location.href = res.url
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="discord-link-container universal-card">
|
||||
<h1>{{ isLinkedCallback ? 'Modrinth account linked' : 'Linking Discord' }}</h1>
|
||||
<p v-if="isLinkedCallback">Your Modrinth account has been linked to the Discord server.</p>
|
||||
<p v-else-if="!error">Connecting your Modrinth account to the Discord server...</p>
|
||||
<p v-else>Discord linking failed. Please try again later.</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.discord-link-container {
|
||||
width: 26rem;
|
||||
max-width: calc(100% - 2rem);
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.discord-link-container h1 {
|
||||
font-size: var(--font-size-xl);
|
||||
margin: 0 0 -1rem 0;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.discord-link-container p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Heading, Section, Text } from '@vue-email/components'
|
||||
|
||||
import StyledEmail from '../shared/StyledEmail.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StyledEmail
|
||||
title="You're invited to the Creator Club"
|
||||
:manual-links="[{ link: '{discord.link_url}', label: 'Link Discord' }]"
|
||||
>
|
||||
<Heading as="h1" class="mb-2 text-2xl font-bold">You're invited to the Creator Club</Heading>
|
||||
|
||||
<Text class="text-base">Hey <span class="no-auto-link">{user.name}</span>!</Text>
|
||||
|
||||
<Text class="text-base"> Your projects just passed 20,000 total downloads, nice! </Text>
|
||||
|
||||
<Text class="text-base">
|
||||
We want to invite you to Modrinth's Creator Club, a space in our discord where you can chat
|
||||
with other creators, share feedback with us, and stay plugged in.
|
||||
</Text>
|
||||
|
||||
<Text class="text-base">
|
||||
To join just link your Discord account through Modrinth and we'll grant access automatically!
|
||||
</Text>
|
||||
|
||||
<Section class="mb-4 mt-4">
|
||||
<Button
|
||||
href="{discord.link_url}"
|
||||
target="_blank"
|
||||
class="text-accentContrast inline-block rounded-[12px] bg-brand pb-3 pl-4 pr-4 pt-3 text-[14px] font-bold"
|
||||
>
|
||||
Join the Creator Club
|
||||
</Button>
|
||||
</Section>
|
||||
</StyledEmail>
|
||||
</template>
|
||||
@@ -36,6 +36,9 @@ export default {
|
||||
'server-invited': () => import('./server/ServerInvited.vue'),
|
||||
'server-invited-no-account': () => import('./server/ServerInvitedNoAccount.vue'),
|
||||
|
||||
// Discord
|
||||
'discord-role-creator-club': () => import('./discord/DiscordRoleCreatorClub.vue'),
|
||||
|
||||
// Organizations
|
||||
'organization-invited': () => import('./organization/OrganizationInvited.vue'),
|
||||
} as Record<string, () => Promise<{ default: Component }>>
|
||||
|
||||
Reference in New Issue
Block a user