mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1696e0d9a | ||
|
|
c0fd7bebbd | ||
|
|
813a62d89d | ||
|
|
6b73118cfc | ||
|
|
202bb20286 | ||
|
|
7c4b4d97dc |
@@ -13,6 +13,8 @@ import { I18nDebugPanel, NotificationPanel } from '@modrinth/ui'
|
||||
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
|
||||
import { useAuth } from './composables/auth'
|
||||
|
||||
const auth = await useAuth()
|
||||
setupProviders(auth)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div class="create-account-card">
|
||||
<h1 class="create-account-title">{{ formatMessage(messages.title) }}</h1>
|
||||
|
||||
<section v-if="requiresDob" class="create-account-section">
|
||||
<label class="create-account-label" for="create-account-dob">
|
||||
{{ formatMessage(messages.dateOfBirthLabel) }}
|
||||
</label>
|
||||
<div class="date-input-wrap">
|
||||
<input
|
||||
id="create-account-dob"
|
||||
v-model="dateOfBirthModel"
|
||||
class="date-input"
|
||||
type="date"
|
||||
:max="maxBirthDate"
|
||||
/>
|
||||
<CalendarIcon class="date-input-icon" />
|
||||
</div>
|
||||
<p class="helper-text">{{ formatMessage(messages.over13HelperText) }}</p>
|
||||
</section>
|
||||
|
||||
<section class="info-panel">
|
||||
<div class="info-panel-icon">
|
||||
<InfoIcon />
|
||||
</div>
|
||||
<div class="info-panel-content">
|
||||
<p>{{ formatMessage(messages.infoPanelText) }}</p>
|
||||
<a class="text-link" :href="sourceCodeUrl" target="_blank" rel="noopener noreferrer">
|
||||
{{ formatMessage(messages.relevantSourceCodeText) }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="create-account-section">
|
||||
<label class="create-account-label" for="create-account-username">
|
||||
{{ formatMessage(messages.usernameOptionalLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="create-account-username"
|
||||
v-model="usernameModel"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="create-account-section">
|
||||
<label class="create-account-label">{{ formatMessage(messages.securityCheckLabel) }}</label>
|
||||
<div class="captcha-wrap">
|
||||
<HCaptcha v-if="globals?.captcha_enabled" :ref="onSetCaptchaRef" v-model="tokenModel" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Checkbox
|
||||
v-model="subscribeModel"
|
||||
class="subscribe-checkbox"
|
||||
:label="formatMessage(messages.subscribeLabel)"
|
||||
:description="formatMessage(messages.subscribeLabel)"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full complete-sign-up-btn"
|
||||
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
||||
@click="onCompleteSignUp()"
|
||||
>
|
||||
{{ formatMessage(messages.completeSignUpButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CalendarIcon, InfoIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Checkbox, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
|
||||
const props = defineProps({
|
||||
dateOfBirth: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subscribe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
globals: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
requiresDob: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
sourceCodeUrl: {
|
||||
type: String,
|
||||
default: 'https://github.com/modrinth/labrinth/blob/main/apps/labrinth/src/routes/internal/flows.rs',
|
||||
},
|
||||
onCompleteSignUp: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onSetCaptchaRef: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:dateOfBirth',
|
||||
'update:username',
|
||||
'update:token',
|
||||
'update:subscribe',
|
||||
])
|
||||
|
||||
const dateOfBirthModel = computed({
|
||||
get: () => props.dateOfBirth,
|
||||
set: (value) => emit('update:dateOfBirth', value),
|
||||
})
|
||||
|
||||
const usernameModel = computed({
|
||||
get: () => props.username,
|
||||
set: (value) => emit('update:username', value),
|
||||
})
|
||||
|
||||
const tokenModel = computed({
|
||||
get: () => props.token,
|
||||
set: (value) => emit('update:token', value),
|
||||
})
|
||||
|
||||
const subscribeModel = computed({
|
||||
get: () => props.subscribe,
|
||||
set: (value) => emit('update:subscribe', value),
|
||||
})
|
||||
|
||||
const maxBirthDate = computed(() => {
|
||||
const date = new Date()
|
||||
date.setFullYear(date.getFullYear() - 13)
|
||||
return date.toISOString().slice(0, 10)
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'auth.create-account.title',
|
||||
defaultMessage: 'Create an Account',
|
||||
},
|
||||
dateOfBirthLabel: {
|
||||
id: 'auth.create-account.date-of-birth.label',
|
||||
defaultMessage: 'Date of birth',
|
||||
},
|
||||
over13HelperText: {
|
||||
id: 'auth.create-account.date-of-birth.over13-helper',
|
||||
defaultMessage: 'You must be over 13 years old to use Modrinth.',
|
||||
},
|
||||
infoPanelText: {
|
||||
id: 'auth.create-account.info-panel.text',
|
||||
defaultMessage: 'We do not store your date of birth, it is only used to confirm your age at sign up.',
|
||||
},
|
||||
relevantSourceCodeText: {
|
||||
id: 'auth.create-account.info-panel.source-code-link',
|
||||
defaultMessage: 'Relevant source code',
|
||||
},
|
||||
usernameOptionalLabel: {
|
||||
id: 'auth.create-account.username.optional-label',
|
||||
defaultMessage: 'Username (Optional)',
|
||||
},
|
||||
usernamePlaceholder: {
|
||||
id: 'auth.create-account.username.placeholder',
|
||||
defaultMessage: 'Enter username',
|
||||
},
|
||||
securityCheckLabel: {
|
||||
id: 'auth.create-account.security-check.label',
|
||||
defaultMessage: 'Security check',
|
||||
},
|
||||
subscribeLabel: {
|
||||
id: 'auth.create-account.subscribe.label',
|
||||
defaultMessage: 'Keep me updated on the cool things Modrinth is working on via email',
|
||||
},
|
||||
completeSignUpButton: {
|
||||
id: 'auth.create-account.complete-sign-up',
|
||||
defaultMessage: 'Complete sign up',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.create-account-card {
|
||||
background: var(--color-raised-bg);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
margin-inline: auto;
|
||||
max-width: 30rem;
|
||||
padding: var(--gap-xl);
|
||||
}
|
||||
|
||||
.create-account-title {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--weight-bold);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.create-account-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.create-account-label {
|
||||
color: var(--color-contrast);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.date-input-wrap {
|
||||
align-items: center;
|
||||
background: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
padding: 0.875rem 1rem;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--color-contrast);
|
||||
font-size: var(--text-lg);
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-input-icon {
|
||||
color: var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--text-lg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
background: color-mix(in oklab, var(--color-brand) 20%, var(--color-bg));
|
||||
border: 1px solid color-mix(in oklab, var(--color-brand) 80%, transparent);
|
||||
border-radius: var(--size-rounded-xl);
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
padding: var(--gap-md);
|
||||
}
|
||||
|
||||
.info-panel-icon {
|
||||
color: var(--color-brand);
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
|
||||
p {
|
||||
color: var(--color-contrast);
|
||||
font-size: var(--text-lg);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-wrap {
|
||||
background: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
min-height: 4.25rem;
|
||||
padding: var(--gap-md);
|
||||
}
|
||||
|
||||
.subscribe-checkbox {
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-xl);
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.complete-sign-up-btn {
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div v-if="subtleLauncherRedirectUri">
|
||||
<iframe
|
||||
:src="subtleLauncherRedirectUri"
|
||||
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||
></iframe>
|
||||
</div>
|
||||
<div v-else>
|
||||
<template v-if="flow && !subtleLauncherRedirectUri">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCodeModel"
|
||||
:maxlength="11"
|
||||
inputmode="numeric"
|
||||
:placeholder="formatMessage(messages.twoFactorCodeInputPlaceholder)"
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="onTwoFactorSignIn()"
|
||||
/>
|
||||
|
||||
<button class="btn btn-primary continue-btn" @click="onTwoFactorSignIn()">
|
||||
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-center text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.signInWithLabel) }}
|
||||
</div>
|
||||
|
||||
<section class="flex flex-col gap-2.5">
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('google', redirectTarget)">
|
||||
<GoogleColorIcon />
|
||||
<span class="ml-1">{{
|
||||
formatMessage(messages.continueWithProvider, { provider: 'Google' })
|
||||
}}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('microsoft', redirectTarget)">
|
||||
<MicrosoftColorIcon />
|
||||
<span class="ml-1">{{
|
||||
formatMessage(messages.continueWithProvider, { provider: 'Microsoft' })
|
||||
}}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('discord', redirectTarget)">
|
||||
<DiscordColorIcon />
|
||||
<span class="ml-1">{{
|
||||
formatMessage(messages.continueWithProvider, { provider: 'Discord' })
|
||||
}}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('github', redirectTarget)">
|
||||
<GitHubColorIcon />
|
||||
<span class="ml-1">{{
|
||||
formatMessage(messages.continueWithProvider, { provider: 'GitHub' })
|
||||
}}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('gitlab', redirectTarget)">
|
||||
<GitLabColorIcon />
|
||||
<span class="ml-1">{{
|
||||
formatMessage(messages.continueWithProvider, { provider: 'GitLab' })
|
||||
}}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('steam', redirectTarget)">
|
||||
<SteamColorIcon />
|
||||
<span class="ml-1">{{
|
||||
formatMessage(messages.continueWithProvider, { provider: 'Steam' })
|
||||
}}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
|
||||
<div class="h-px w-full bg-surface-5"></div>
|
||||
|
||||
<section class="auth-form">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="emailModel"
|
||||
:icon="MailIcon"
|
||||
type="text"
|
||||
inputmode="email"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.emailUsernameLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="passwordModel"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<HCaptcha
|
||||
v-if="globals?.captcha_enabled && emailModel && passwordModel"
|
||||
:ref="onSetCaptchaRef"
|
||||
v-model="tokenModel"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full"
|
||||
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
||||
@click="onPasswordSignIn()"
|
||||
>
|
||||
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="auth-form__additional-options !text-base">
|
||||
<IntlFormatted :message-id="messages.additionalOptionsLabel">
|
||||
<template #forgot-password-link="{ children }">
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/reset-password',
|
||||
query: routeQuery,
|
||||
}"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #create-account-link="{ children }">
|
||||
<NuxtLink
|
||||
class="inline text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-up',
|
||||
query: routeQuery,
|
||||
}"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
import { getAuthUrl } from '@/composables/auth.ts'
|
||||
|
||||
const props = defineProps({
|
||||
subtleLauncherRedirectUri: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
flow: {
|
||||
default: '',
|
||||
},
|
||||
redirectTarget: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
routeQuery: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
globals: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
twoFactorCode: {
|
||||
default: null,
|
||||
},
|
||||
onPasswordSignIn: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onTwoFactorSignIn: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onSetCaptchaRef: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:email',
|
||||
'update:password',
|
||||
'update:token',
|
||||
'update:twoFactorCode',
|
||||
])
|
||||
|
||||
const emailModel = computed({
|
||||
get: () => props.email,
|
||||
set: (value) => emit('update:email', value),
|
||||
})
|
||||
|
||||
const passwordModel = computed({
|
||||
get: () => props.password,
|
||||
set: (value) => emit('update:password', value),
|
||||
})
|
||||
|
||||
const tokenModel = computed({
|
||||
get: () => props.token,
|
||||
set: (value) => emit('update:token', value),
|
||||
})
|
||||
|
||||
const twoFactorCodeModel = computed({
|
||||
get: () => props.twoFactorCode,
|
||||
set: (value) => emit('update:twoFactorCode', value),
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
additionalOptionsLabel: {
|
||||
id: 'auth.sign-in.additional-options',
|
||||
defaultMessage:
|
||||
"<forgot-password-link>Forgot password</forgot-password-link> • Don't have an account? <create-account-link>Sign up</create-account-link>",
|
||||
},
|
||||
signInWithLabel: {
|
||||
id: 'auth.sign-in.sign-in-with',
|
||||
defaultMessage: 'Sign into Modrinth',
|
||||
},
|
||||
twoFactorCodeInputPlaceholder: {
|
||||
id: 'auth.sign-in.2fa.placeholder',
|
||||
defaultMessage: 'Enter code...',
|
||||
},
|
||||
twoFactorCodeLabel: {
|
||||
id: 'auth.sign-in.2fa.label',
|
||||
defaultMessage: 'Enter two-factor code',
|
||||
},
|
||||
twoFactorCodeLabelDescription: {
|
||||
id: 'auth.sign-in.2fa.description',
|
||||
defaultMessage: 'Please enter a two-factor code to proceed.',
|
||||
},
|
||||
continueWithProvider: {
|
||||
id: 'auth.continue-with-provider',
|
||||
defaultMessage: 'Continue with {provider}',
|
||||
},
|
||||
continueWithEmail: {
|
||||
id: 'auth.sign-in.continue-with-email',
|
||||
defaultMessage: 'Continue with Email',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-center text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.signUpWithTitle) }}
|
||||
</div>
|
||||
<section class="flex flex-col gap-2.5">
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('google', redirectTarget)">
|
||||
<GoogleColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Google' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('microsoft', redirectTarget)">
|
||||
<MicrosoftColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Microsoft' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('discord', redirectTarget)">
|
||||
<DiscordColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Discord' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<template v-if="showOtherOptions">
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('github', redirectTarget)">
|
||||
<GitHubColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'GitHub' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('gitlab', redirectTarget)">
|
||||
<GitLabColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'GitLab' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a class="!shadow-none" :href="getAuthUrl('steam', redirectTarget)">
|
||||
<SteamColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Steam' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<button
|
||||
class="mx-auto -mb-3 bg-transparent pt-1 text-center text-base font-semibold text-secondary transition-all hover:text-primary"
|
||||
@click="onToggleOtherOptions()"
|
||||
>
|
||||
{{
|
||||
showOtherOptions
|
||||
? formatMessage(messages.showFewerOptions)
|
||||
: formatMessage(messages.showOtherOptions)
|
||||
}}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div class="h-px w-full bg-surface-5"></div>
|
||||
|
||||
<section class="flex flex-col gap-2.5">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="emailModel"
|
||||
:icon="MailIcon"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
:placeholder="formatMessage(commonMessages.emailLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="passwordModel"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model="subscribeModel"
|
||||
class="subscribe-btn"
|
||||
:label="formatMessage(messages.subscribeLabel)"
|
||||
:description="formatMessage(messages.subscribeLabel)"
|
||||
/>
|
||||
|
||||
<p v-if="!routeQuery.launcher">
|
||||
<IntlFormatted :message-id="messages.legalDisclaimer">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/legal/terms" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-policy-link="{ children }">
|
||||
<NuxtLink to="/legal/privacy" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<HCaptcha
|
||||
v-if="globals?.captcha_enabled && emailModel && passwordModel"
|
||||
:ref="onSetCaptchaRef"
|
||||
v-model="tokenModel"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full"
|
||||
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
||||
@click="onCreateAccount()"
|
||||
>
|
||||
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="auth-form__additional-options">
|
||||
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-in',
|
||||
query: routeQuery,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
import { getAuthUrl } from '@/composables/auth.ts'
|
||||
|
||||
const props = defineProps({
|
||||
redirectTarget: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showOtherOptions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
routeQuery: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
globals: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subscribe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onToggleOtherOptions: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onCreateAccount: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onSetCaptchaRef: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:email', 'update:password', 'update:token', 'update:subscribe'])
|
||||
|
||||
const emailModel = computed({
|
||||
get: () => props.email,
|
||||
set: (value) => emit('update:email', value),
|
||||
})
|
||||
|
||||
const passwordModel = computed({
|
||||
get: () => props.password,
|
||||
set: (value) => emit('update:password', value),
|
||||
})
|
||||
|
||||
const tokenModel = computed({
|
||||
get: () => props.token,
|
||||
set: (value) => emit('update:token', value),
|
||||
})
|
||||
|
||||
const subscribeModel = computed({
|
||||
get: () => props.subscribe,
|
||||
set: (value) => emit('update:subscribe', value),
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
signUpWithTitle: {
|
||||
id: 'auth.sign-up.title.sign-up-with',
|
||||
defaultMessage: 'Create an Account',
|
||||
},
|
||||
continueWithProvider: {
|
||||
id: 'auth.continue-with-provider',
|
||||
defaultMessage: 'Continue with {provider}',
|
||||
},
|
||||
subscribeLabel: {
|
||||
id: 'auth.sign-up.subscribe.label',
|
||||
defaultMessage: 'Subscribe to updates about Modrinth',
|
||||
},
|
||||
legalDisclaimer: {
|
||||
id: 'auth.sign-up.legal-dislaimer',
|
||||
defaultMessage:
|
||||
"By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
|
||||
},
|
||||
alreadyHaveAccountLabel: {
|
||||
id: 'auth.sign-up.sign-in-option.title',
|
||||
defaultMessage: 'Already have an account?',
|
||||
},
|
||||
continueWithEmail: {
|
||||
id: 'auth.sign-up.continue-with-email',
|
||||
defaultMessage: 'Continue with Email',
|
||||
},
|
||||
showFewerOptions: {
|
||||
id: 'auth.sign-up.show-fewer-options',
|
||||
defaultMessage: 'Show fewer options',
|
||||
},
|
||||
showOtherOptions: {
|
||||
id: 'auth.sign-up.show-other-options',
|
||||
defaultMessage: 'Show other options',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -164,9 +164,7 @@ defineExpose({
|
||||
show,
|
||||
})
|
||||
|
||||
const auth = (await useAuth()) as Ref<{
|
||||
user: { id: string; username: string; avatar_url: string } | null
|
||||
}>
|
||||
const auth = await useAuth()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
|
||||
+1
-2
@@ -120,7 +120,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
|
||||
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
|
||||
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.js'
|
||||
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.ts'
|
||||
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
|
||||
|
||||
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees, saveStateToStorage } =
|
||||
@@ -193,7 +193,6 @@ async function saveVenmoHandle() {
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error auth.js is not typed
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
initialVenmoHandle.value = venmoHandle.value.trim()
|
||||
|
||||
+1
-1
@@ -358,8 +358,8 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
|
||||
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
|
||||
import { useAuth } from '@/composables/auth.js'
|
||||
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
|
||||
import { useAuth } from '~/composables/auth.ts'
|
||||
|
||||
const debug = useDebugLogger('TremendousDetailsStage')
|
||||
const { withdrawData, maxWithdrawAmount, availableMethods, paymentOptions, calculateFees } =
|
||||
|
||||
@@ -190,7 +190,6 @@ import {
|
||||
LinkIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation'
|
||||
import { type OverflowMenuOption, useFormatDateTime } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
@@ -198,6 +197,8 @@ import {
|
||||
getProjectTypeIcon,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
@@ -26,11 +26,11 @@ import {
|
||||
getProjectTypeIcon,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useFormatDateTime,
|
||||
} from '@modrinth/ui'
|
||||
import { NavTabs } from '@modrinth/ui'
|
||||
import {
|
||||
capitalizeString,
|
||||
formatProjectType,
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ const currentProjectId = computed(() => projectV3.value?.id)
|
||||
const { selectedProjectId, selectedVersionId } = injectServerCompatibilityContext()
|
||||
const { labrinth } = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const auth = (await useAuth()) as { user?: { id: string } }
|
||||
const auth = await useAuth()
|
||||
|
||||
interface VersionInfo {
|
||||
id: string
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
export const useAuth = async (oldToken = null) => {
|
||||
const auth = useState('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
headers: {},
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken = null) => {
|
||||
const auth = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const authCookie = useCookie('auth-token', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
authCookie.value = oldToken
|
||||
}
|
||||
|
||||
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = route.query.code
|
||||
}
|
||||
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${route.query.code}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (authCookie.value) {
|
||||
auth.token = authCookie.value
|
||||
|
||||
if (!auth.token || !auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
auth.token = session.session
|
||||
authCookie.value = auth.token
|
||||
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getSignInRedirectPath = (route) => {
|
||||
const fullPath = route.fullPath
|
||||
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
|
||||
return '/dashboard'
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export const getSignInRouteObj = (route, redirectOverride) => ({
|
||||
path: '/auth/sign-in',
|
||||
query: {
|
||||
redirect: redirectOverride ?? getSignInRedirectPath(route),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? getLauncherRedirectUrl(route)
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
startLoading()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route) => {
|
||||
const usesLocalhostRedirectionScheme =
|
||||
['4', '6'].includes(route.query.ipver) && Number(route.query.port) < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${route.query.ipver === '4' ? '127.0.0.1' : '[::1]'}:${route.query.port}`
|
||||
: `https://launcher-files.modrinth.com`
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { LocationQueryValue, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import type { CookieOptions } from '#app'
|
||||
|
||||
type AuthState = {
|
||||
user: Labrinth.Users.v2.User | null
|
||||
token: string
|
||||
}
|
||||
|
||||
type QueryValue = LocationQueryValue | LocationQueryValue[] | undefined
|
||||
type FullPathRoute = Pick<RouteLocationNormalizedLoaded, 'fullPath'>
|
||||
type LauncherRoute = Pick<RouteLocationNormalizedLoaded, 'query'>
|
||||
|
||||
const AUTH_COOKIE_OPTIONS = {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
} satisfies CookieOptions<string | null>
|
||||
|
||||
const getQueryString = (value: QueryValue) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null
|
||||
}
|
||||
return value ?? null
|
||||
}
|
||||
|
||||
export const useAuth = async (oldToken: string | null | undefined = null) => {
|
||||
const auth = useState<AuthState>('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken: string | null | undefined = null) => {
|
||||
const auth: AuthState = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const authCookie = useCookie<string | null>('auth-token', AUTH_COOKIE_OPTIONS)
|
||||
const authCode = getQueryString(route.query.code)
|
||||
|
||||
if (oldToken) {
|
||||
authCookie.value = oldToken
|
||||
}
|
||||
|
||||
if (authCode) {
|
||||
authCookie.value = authCode
|
||||
}
|
||||
|
||||
if (authCookie.value) {
|
||||
auth.token = authCookie.value
|
||||
|
||||
if (!auth.token || !auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = (await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as Labrinth.Users.v2.User
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = (await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as { session: string }
|
||||
|
||||
auth.token = session.session
|
||||
authCookie.value = auth.token
|
||||
|
||||
auth.user = (await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as Labrinth.Users.v2.User
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getSignInRedirectPath = (route: FullPathRoute) => {
|
||||
const fullPath = route.fullPath
|
||||
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
|
||||
return '/dashboard'
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export const getSignInRouteObj = (route: FullPathRoute, redirectOverride?: string | null) => ({
|
||||
path: '/auth/sign-in',
|
||||
query: {
|
||||
redirect: redirectOverride ?? getSignInRedirectPath(route),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthUrl = (provider: string, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
const launcher = getQueryString(route.query.launcher)
|
||||
|
||||
const fullURL = launcher
|
||||
? getLauncherRedirectUrl(route)
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider: string) => {
|
||||
startLoading()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route: LauncherRoute) => {
|
||||
const ipver = getQueryString(route.query.ipver)
|
||||
const port = Number(getQueryString(route.query.port))
|
||||
const usesLocalhostRedirectionScheme = ['4', '6'].includes(ipver ?? '') && port < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${ipver === '4' ? '127.0.0.1' : '[::1]'}:${port}`
|
||||
: 'https://launcher-files.modrinth.com'
|
||||
}
|
||||
@@ -762,7 +762,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
|
||||
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
|
||||
import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { errors as generatedStateErrors } from '~/generated/state.json'
|
||||
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
|
||||
|
||||
|
||||
@@ -224,6 +224,9 @@
|
||||
"auth.authorize.redirect-url": {
|
||||
"message": "You will be redirected to <redirect-url>{url}</redirect-url>"
|
||||
},
|
||||
"auth.continue-with-provider": {
|
||||
"message": "Continue with {provider}"
|
||||
},
|
||||
"auth.reset-password.method-choice.action": {
|
||||
"message": "Send recovery email"
|
||||
},
|
||||
@@ -267,25 +270,28 @@
|
||||
"message": "Enter code..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Forgot password?</forgot-password-link> • <create-account-link>Create an account</create-account-link>"
|
||||
"message": "<forgot-password-link>Forgot password</forgot-password-link> • Don't have an account? <create-account-link>Sign up</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.continue-with-email": {
|
||||
"message": "Continue with Email"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Sign in with"
|
||||
"message": "Sign into Modrinth"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Sign In"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Or use a password"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Create account"
|
||||
"auth.sign-up.continue-with-email": {
|
||||
"message": "Continue with Email"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Passwords do not match!"
|
||||
"auth.sign-up.show-fewer-options": {
|
||||
"message": "Show fewer options"
|
||||
},
|
||||
"auth.sign-up.show-other-options": {
|
||||
"message": "Show other options"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Already have an account?"
|
||||
@@ -296,11 +302,8 @@
|
||||
"auth.sign-up.title": {
|
||||
"message": "Sign Up"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Or create an account yourself"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Sign up with"
|
||||
"message": "Create an Account"
|
||||
},
|
||||
"auth.verify-email.action.account-settings": {
|
||||
"message": "Account settings"
|
||||
|
||||
@@ -1121,7 +1121,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
|
||||
import MessageBanner from '~/components/ui/MessageBanner.vue'
|
||||
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
||||
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
|
||||
@@ -308,7 +308,7 @@ import {
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -443,7 +443,7 @@ import { formatBytes, renderHighlightedString } from '@modrinth/utils'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { inferVersionInfo } from '~/helpers/infer'
|
||||
import { createDataPackVersion } from '~/helpers/package.js'
|
||||
|
||||
@@ -269,7 +269,7 @@ import {
|
||||
import { onMounted, useTemplateRef } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -8,14 +8,15 @@ useSeoMeta({
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage class="auth-container universal-card" />
|
||||
<div class="grid min-h-[calc(100vh-4.5rem)] place-items-center pb-20">
|
||||
<NuxtPage class="auth-container universal-card border border-solid border-surface-5" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.auth-container {
|
||||
width: 26rem;
|
||||
width: 28rem;
|
||||
max-width: calc(100% - 2rem);
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
@@ -45,24 +46,6 @@ useSeoMeta({
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.third-party {
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.third-party .btn {
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.third-party .btn svg {
|
||||
margin-right: var(--gap-sm);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 25.5rem) {
|
||||
.third-party .btn {
|
||||
grid-column: 1 / 3;
|
||||
@@ -84,7 +67,7 @@ useSeoMeta({
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--gap-md);
|
||||
gap: var(--gap-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ import {
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useAuth } from '@/composables/auth.js'
|
||||
import { useScopes } from '@/composables/auth/scopes.ts'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div v-if="subtleLauncherRedirectUri">
|
||||
<iframe
|
||||
:src="subtleLauncherRedirectUri"
|
||||
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||
></iframe>
|
||||
</div>
|
||||
<CreateAccountView
|
||||
v-else
|
||||
v-model:date-of-birth="dateOfBirth"
|
||||
v-model:username="username"
|
||||
v-model:token="token"
|
||||
v-model:subscribe="subscribe"
|
||||
:globals="globals"
|
||||
:requires-dob="requiresDob"
|
||||
:on-complete-sign-up="completeOAuthSignUp"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { commonMessages, defineMessages, injectModrinthClient, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
import CreateAccountView from '@/components/ui/auth/CreateAccount.vue'
|
||||
import { getLauncherRedirectUrl } from '@/composables/auth.ts'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const route = useNativeRoute()
|
||||
const auth = await useAuth()
|
||||
|
||||
const messages = defineMessages({
|
||||
createAccountTitle: {
|
||||
id: 'auth.create-account.page-title',
|
||||
defaultMessage: 'Create Account',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
title() {
|
||||
return `${formatMessage(messages.createAccountTitle)} - Modrinth`
|
||||
},
|
||||
})
|
||||
|
||||
const requiresDob = computed(() => {
|
||||
const raw = route.query.requires_dob
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return value === 'true' || value === '1'
|
||||
})
|
||||
|
||||
const oauthFlowState = computed(() => {
|
||||
const state = route.query.state
|
||||
const value = Array.isArray(state) ? state[0] : state
|
||||
return typeof value === 'string' ? value : ''
|
||||
})
|
||||
|
||||
const defaultUsername = computed(() => {
|
||||
const queryUsername = route.query.username
|
||||
const value = Array.isArray(queryUsername) ? queryUsername[0] : queryUsername
|
||||
return typeof value === 'string' && value.length > 0 ? value : 'user'
|
||||
})
|
||||
|
||||
const dateOfBirth = ref('')
|
||||
const username = ref(defaultUsername.value)
|
||||
const token = ref('')
|
||||
const subscribe = ref(false)
|
||||
const subtleLauncherRedirectUri = ref()
|
||||
|
||||
const captcha = ref()
|
||||
const setCaptchaRef = (captchaRef) => {
|
||||
captcha.value = captchaRef
|
||||
}
|
||||
|
||||
const { data: globals } = useQuery({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await client.labrinth.globals_internal.get()
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true, tax_compliance_thresholds: {} }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
async function completeOAuthSignUp() {
|
||||
startLoading()
|
||||
try {
|
||||
if (!oauthFlowState.value) {
|
||||
throw new Error('Missing OAuth flow state')
|
||||
}
|
||||
|
||||
const res = await client.labrinth.auth_v2.createOAuthAccount({
|
||||
username: username.value.trim() || defaultUsername.value,
|
||||
state: oauthFlowState.value,
|
||||
challenge: token.value,
|
||||
sign_up_newsletter: subscribe.value,
|
||||
})
|
||||
|
||||
await finishSignIn(res.session)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function finishSignIn(sessionToken) {
|
||||
if (route.query.launcher) {
|
||||
let token = sessionToken
|
||||
if (!token) {
|
||||
token = auth.value.token
|
||||
}
|
||||
|
||||
const redirectUrl = `${getLauncherRedirectUrl(route)}/?code=${token}`
|
||||
|
||||
if (redirectUrl.startsWith('https://launcher-files.modrinth.com/')) {
|
||||
await navigateTo(redirectUrl, {
|
||||
external: true,
|
||||
})
|
||||
} else {
|
||||
subtleLauncherRedirectUri.value = redirectUrl
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (sessionToken) {
|
||||
await useAuth(sessionToken)
|
||||
await useUser()
|
||||
queryClient.clear()
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
const redirect = decodeURIComponent(route.query.redirect)
|
||||
await navigateTo(redirect, {
|
||||
replace: true,
|
||||
})
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -76,7 +76,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
@@ -1,154 +1,32 @@
|
||||
<template>
|
||||
<div v-if="subtleLauncherRedirectUri">
|
||||
<iframe
|
||||
:src="subtleLauncherRedirectUri"
|
||||
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||
></iframe>
|
||||
</div>
|
||||
<div v-else>
|
||||
<template v-if="flow && !subtleLauncherRedirectUri">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCode"
|
||||
:maxlength="11"
|
||||
inputmode="numeric"
|
||||
:placeholder="formatMessage(messages.twoFactorCodeInputPlaceholder)"
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="begin2FASignIn"
|
||||
/>
|
||||
|
||||
<button class="btn btn-primary continue-btn" @click="begin2FASignIn">
|
||||
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>{{ formatMessage(messages.signInWithLabel) }}</h1>
|
||||
|
||||
<section class="third-party">
|
||||
<a class="btn" :href="getAuthUrl('discord', redirectTarget)">
|
||||
<DiscordColorIcon />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('github', redirectTarget)">
|
||||
<GitHubColorIcon />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('microsoft', redirectTarget)">
|
||||
<MicrosoftColorIcon />
|
||||
<span>Microsoft</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('google', redirectTarget)">
|
||||
<GoogleColorIcon />
|
||||
<span>Google</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('steam', redirectTarget)">
|
||||
<SteamColorIcon />
|
||||
<span>Steam</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('gitlab', redirectTarget)">
|
||||
<GitLabColorIcon />
|
||||
<span>GitLab</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<h1>{{ formatMessage(messages.usePasswordLabel) }}</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="email"
|
||||
:icon="MailIcon"
|
||||
type="text"
|
||||
inputmode="email"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.emailUsernameLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="password"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<HCaptcha v-if="globals?.captcha_enabled" ref="captcha" v-model="token" />
|
||||
|
||||
<button
|
||||
class="btn btn-primary continue-btn centered-btn"
|
||||
:disabled="globals?.captcha_enabled ? !token : false"
|
||||
@click="beginPasswordSignIn()"
|
||||
>
|
||||
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
|
||||
<div class="auth-form__additional-options">
|
||||
<IntlFormatted :message-id="messages.additionalOptionsLabel">
|
||||
<template #forgot-password-link="{ children }">
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/reset-password',
|
||||
query: route.query,
|
||||
}"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #create-account-link="{ children }">
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-up',
|
||||
query: route.query,
|
||||
}"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
<SignInView
|
||||
v-model:email="email"
|
||||
v-model:password="password"
|
||||
v-model:token="token"
|
||||
v-model:two-factor-code="twoFactorCode"
|
||||
:subtle-launcher-redirect-uri="subtleLauncherRedirectUri"
|
||||
:flow="flow"
|
||||
:redirect-target="redirectTarget"
|
||||
:route-query="route.query"
|
||||
:globals="globals"
|
||||
:on-password-sign-in="beginPasswordSignIn"
|
||||
:on-two-factor-sign-in="begin2FASignIn"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import { getAuthUrl, getLauncherRedirectUrl } from '@/composables/auth.js'
|
||||
import SignInView from '@/components/ui/auth/SignIn.vue'
|
||||
import { getLauncherRedirectUrl } from '@/composables/auth.ts'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -156,35 +34,10 @@ const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
additionalOptionsLabel: {
|
||||
id: 'auth.sign-in.additional-options',
|
||||
defaultMessage:
|
||||
'<forgot-password-link>Forgot password?</forgot-password-link> • <create-account-link>Create an account</create-account-link>',
|
||||
},
|
||||
signInWithLabel: {
|
||||
id: 'auth.sign-in.sign-in-with',
|
||||
defaultMessage: 'Sign in with',
|
||||
},
|
||||
signInTitle: {
|
||||
id: 'auth.sign-in.title',
|
||||
defaultMessage: 'Sign In',
|
||||
},
|
||||
twoFactorCodeInputPlaceholder: {
|
||||
id: 'auth.sign-in.2fa.placeholder',
|
||||
defaultMessage: 'Enter code...',
|
||||
},
|
||||
twoFactorCodeLabel: {
|
||||
id: 'auth.sign-in.2fa.label',
|
||||
defaultMessage: 'Enter two-factor code',
|
||||
},
|
||||
twoFactorCodeLabelDescription: {
|
||||
id: 'auth.sign-in.2fa.description',
|
||||
defaultMessage: 'Please enter a two-factor code to proceed.',
|
||||
},
|
||||
usePasswordLabel: {
|
||||
id: 'auth.sign-in.use-password',
|
||||
defaultMessage: 'Or use a password',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
@@ -199,7 +52,7 @@ const route = useNativeRoute()
|
||||
const redirectTarget = route.query.redirect || ''
|
||||
const subtleLauncherRedirectUri = ref()
|
||||
|
||||
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||
if (route.query.code) {
|
||||
await finishSignIn()
|
||||
}
|
||||
|
||||
@@ -208,6 +61,9 @@ if (auth.value.user) {
|
||||
}
|
||||
|
||||
const captcha = ref()
|
||||
const setCaptchaRef = (captchaRef) => {
|
||||
captcha.value = captchaRef
|
||||
}
|
||||
|
||||
const { data: globals } = useQuery({
|
||||
queryKey: ['auth-globals'],
|
||||
|
||||
@@ -1,156 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ formatMessage(messages.signUpWithTitle) }}</h1>
|
||||
|
||||
<section class="third-party">
|
||||
<a class="btn discord-btn" :href="getAuthUrl('discord', redirectTarget)">
|
||||
<DiscordColorIcon />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('github', redirectTarget)">
|
||||
<GitHubColorIcon />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('microsoft', redirectTarget)">
|
||||
<MicrosoftColorIcon />
|
||||
<span>Microsoft</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('google', redirectTarget)">
|
||||
<GoogleColorIcon />
|
||||
<span>Google</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('steam', redirectTarget)">
|
||||
<SteamColorIcon />
|
||||
<span>Steam</span>
|
||||
</a>
|
||||
<a class="btn" :href="getAuthUrl('gitlab', redirectTarget)">
|
||||
<GitLabColorIcon />
|
||||
<span>GitLab</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<h1>{{ formatMessage(messages.createAccountTitle) }}</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="email"
|
||||
:icon="MailIcon"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.emailLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="username" hidden>{{ formatMessage(commonMessages.usernameLabel) }}</label>
|
||||
<StyledInput
|
||||
id="username"
|
||||
v-model="username"
|
||||
:icon="UserIcon"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.usernameLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="password"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="confirm-password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(commonMessages.confirmPasswordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model="subscribe"
|
||||
class="subscribe-btn"
|
||||
:label="formatMessage(messages.subscribeLabel)"
|
||||
:description="formatMessage(messages.subscribeLabel)"
|
||||
/>
|
||||
|
||||
<p v-if="!route.query.launcher">
|
||||
<IntlFormatted :message-id="messages.legalDisclaimer">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/legal/terms" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-policy-link="{ children }">
|
||||
<NuxtLink to="/legal/privacy" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<HCaptcha v-if="globals?.captcha_enabled" ref="captcha" v-model="token" />
|
||||
|
||||
<button
|
||||
class="btn btn-primary continue-btn centered-btn"
|
||||
:disabled="globals?.captcha_enabled ? !token : false"
|
||||
@click="createAccount"
|
||||
>
|
||||
{{ formatMessage(messages.createAccountButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
|
||||
<div class="auth-form__additional-options">
|
||||
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-in',
|
||||
query: route.query,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<SignUpView
|
||||
v-model:email="email"
|
||||
v-model:password="password"
|
||||
v-model:token="token"
|
||||
v-model:subscribe="subscribe"
|
||||
:redirect-target="redirectTarget"
|
||||
:show-other-options="showOtherOptions"
|
||||
:route-query="route.query"
|
||||
:globals="globals"
|
||||
:on-toggle-other-options="toggleOtherOptions"
|
||||
:on-create-account="createAccount"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import { getAuthUrl } from '@/composables/auth.js'
|
||||
import SignUpView from '@/components/ui/auth/SignUp.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -161,31 +35,6 @@ const messages = defineMessages({
|
||||
id: 'auth.sign-up.title',
|
||||
defaultMessage: 'Sign Up',
|
||||
},
|
||||
signUpWithTitle: {
|
||||
id: 'auth.sign-up.title.sign-up-with',
|
||||
defaultMessage: 'Sign up with',
|
||||
},
|
||||
createAccountTitle: {
|
||||
id: 'auth.sign-up.title.create-account',
|
||||
defaultMessage: 'Or create an account yourself',
|
||||
},
|
||||
subscribeLabel: {
|
||||
id: 'auth.sign-up.subscribe.label',
|
||||
defaultMessage: 'Subscribe to updates about Modrinth',
|
||||
},
|
||||
legalDisclaimer: {
|
||||
id: 'auth.sign-up.legal-dislaimer',
|
||||
defaultMessage:
|
||||
"By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
|
||||
},
|
||||
createAccountButton: {
|
||||
id: 'auth.sign-up.action.create-account',
|
||||
defaultMessage: 'Create account',
|
||||
},
|
||||
alreadyHaveAccountLabel: {
|
||||
id: 'auth.sign-up.sign-in-option.title',
|
||||
defaultMessage: 'Already have an account?',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
@@ -196,12 +45,19 @@ const auth = await useAuth()
|
||||
const route = useNativeRoute()
|
||||
|
||||
const redirectTarget = route.query.redirect
|
||||
const showOtherOptions = ref(false)
|
||||
|
||||
if (auth.value.user) {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
|
||||
const captcha = ref()
|
||||
const setCaptchaRef = (captchaRef) => {
|
||||
captcha.value = captchaRef
|
||||
}
|
||||
const toggleOtherOptions = () => {
|
||||
showOtherOptions.value = !showOtherOptions.value
|
||||
}
|
||||
|
||||
const { data: globals } = useQuery({
|
||||
queryKey: ['auth-globals'],
|
||||
@@ -216,29 +72,25 @@ const { data: globals } = useQuery({
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const token = ref('')
|
||||
const subscribe = ref(false)
|
||||
|
||||
function generateUsernameFromEmail(emailAddress) {
|
||||
const [localPart = '', domainPart = ''] = emailAddress.trim().toLowerCase().split('@')
|
||||
const sanitized = `${localPart}_${domainPart}`
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
|
||||
return (sanitized || 'user').slice(0, 39)
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
if (confirmPassword.value !== password.value) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: formatMessage({
|
||||
id: 'auth.sign-up.notification.password-mismatch.text',
|
||||
defaultMessage: 'Passwords do not match!',
|
||||
}),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
}
|
||||
|
||||
const res = await client.labrinth.auth_v2.createAccount({
|
||||
username: username.value,
|
||||
username: generateUsernameFromEmail(email.value),
|
||||
password: password.value,
|
||||
email: email.value,
|
||||
challenge: token.value,
|
||||
|
||||
@@ -54,7 +54,6 @@ import { Avatar, injectModrinthClient } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
|
||||
import { useAuth } from '~/composables/auth.js'
|
||||
|
||||
const createOrgModal = ref(null)
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ import SteamIcon from 'assets/icons/auth/sso-steam.svg'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.js'
|
||||
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.ts'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
|
||||
@@ -521,7 +521,7 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
|
||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { reportUser } from '~/utils/report-helpers.ts'
|
||||
|
||||
const data = useNuxtApp()
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import { type AuthProvider, provideAuth } from '@modrinth/ui'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
import { getSignInRedirectPath } from '~/composables/auth.js'
|
||||
import { getSignInRedirectPath } from '~/composables/auth.ts'
|
||||
|
||||
export function setupAuthProvider(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -57,6 +57,23 @@ export class LabrinthAuthV2Module extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new account from an OAuth callback flow state
|
||||
*
|
||||
* @param data - OAuth account creation data
|
||||
* @returns Promise resolving to a session response
|
||||
*/
|
||||
public async createOAuthAccount(
|
||||
data: Labrinth.Auth.v2.CreateOAuthAccountRequest,
|
||||
): Promise<Labrinth.Auth.v2.CreateOAuthAccountResponse> {
|
||||
return this.client.request<Labrinth.Auth.v2.CreateOAuthAccountResponse>(`/auth/create/oauth`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a password reset flow by sending a recovery email
|
||||
*
|
||||
|
||||
@@ -278,6 +278,17 @@ export namespace Labrinth {
|
||||
session: string
|
||||
}
|
||||
|
||||
export type CreateOAuthAccountRequest = {
|
||||
username: string
|
||||
state: string
|
||||
challenge: string
|
||||
sign_up_newsletter: boolean
|
||||
}
|
||||
|
||||
export type CreateOAuthAccountResponse = {
|
||||
session: string
|
||||
}
|
||||
|
||||
export type ResetPasswordRequest = {
|
||||
username: string
|
||||
challenge: string
|
||||
|
||||
Reference in New Issue
Block a user