mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: implement under 13 DOB guard and email/password validation route
This commit is contained in:
@@ -15,25 +15,29 @@
|
||||
<input
|
||||
id="create-account-dob"
|
||||
v-model="dateOfBirthModel"
|
||||
class="scheme-dark w-full border-0 bg-surface-4 text-lg text-contrast outline-none [color-scheme:dark]"
|
||||
class="scheme-dark w-full border-0 bg-surface-4 text-lg text-primary outline-none [color-scheme:dark]"
|
||||
type="date"
|
||||
:max="maxBirthDate"
|
||||
/>
|
||||
<div>You must be over 13 years old to use Modrinth.</div>
|
||||
<Admonition type="info">
|
||||
<div class="flex flex-col gap-1.5 leading-normal">
|
||||
<div>
|
||||
{{ formatMessage(messages.infoPanelText) }}
|
||||
<div>
|
||||
{{ formatMessage(messages.over13HelperText) }}
|
||||
</div>
|
||||
<Admonition :type="'info'">
|
||||
<template #header>
|
||||
<div class="-mb-2 flex flex-col gap-1.5 font-normal leading-normal">
|
||||
<div>
|
||||
{{ formatMessage(messages.infoPanelText) }}
|
||||
</div>
|
||||
<a
|
||||
class="w-fit text-link underline"
|
||||
:href="sourceCodeUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ formatMessage(messages.relevantSourceCodeText) }}
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="w-fit text-link"
|
||||
:href="sourceCodeUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ formatMessage(messages.relevantSourceCodeText) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</Admonition>
|
||||
</section>
|
||||
|
||||
@@ -71,7 +75,7 @@
|
||||
<button
|
||||
class="!w-full font-bold"
|
||||
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
||||
@click="onCompleteSignUp()"
|
||||
@click="onCompleteSignUpClick"
|
||||
>
|
||||
{{ formatMessage(messages.completeSignUpButton) }}
|
||||
</button>
|
||||
@@ -85,6 +89,7 @@ import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
@@ -120,7 +125,7 @@ const props = defineProps({
|
||||
sourceCodeUrl: {
|
||||
type: String,
|
||||
default:
|
||||
'https://github.com/modrinth/labrinth/blob/main/apps/labrinth/src/routes/internal/flows.rs',
|
||||
'https://github.com/modrinth/code/blob/main/apps/frontend/src/components/ui/auth/CreateAccount.vue',
|
||||
},
|
||||
onCompleteSignUp: {
|
||||
type: Function,
|
||||
@@ -165,8 +170,40 @@ const maxBirthDate = computed(() => {
|
||||
return date.toISOString().slice(0, 10)
|
||||
})
|
||||
|
||||
const isDateOfBirthMissing = computed(() => props.requiresDob && dateOfBirthModel.value === '')
|
||||
|
||||
const isUnder13 = computed(
|
||||
() =>
|
||||
props.requiresDob &&
|
||||
dateOfBirthModel.value !== '' &&
|
||||
dateOfBirthModel.value > maxBirthDate.value,
|
||||
)
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
function onCompleteSignUpClick() {
|
||||
if (isDateOfBirthMissing.value) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.dateOfBirthRequiredTitle),
|
||||
text: formatMessage(messages.dateOfBirthRequiredText),
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isUnder13.value) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.ageRequirementWarningTitle),
|
||||
text: formatMessage(messages.under13HelperText),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
props.onCompleteSignUp()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'auth.create-account.title',
|
||||
@@ -176,10 +213,26 @@ const messages = defineMessages({
|
||||
id: 'auth.create-account.date-of-birth.label',
|
||||
defaultMessage: 'Date of birth',
|
||||
},
|
||||
dateOfBirthRequiredTitle: {
|
||||
id: 'auth.create-account.date-of-birth.required.title',
|
||||
defaultMessage: 'Date of birth required',
|
||||
},
|
||||
dateOfBirthRequiredText: {
|
||||
id: 'auth.create-account.date-of-birth.required.text',
|
||||
defaultMessage: 'Please enter your date of birth before continuing.',
|
||||
},
|
||||
over13HelperText: {
|
||||
id: 'auth.create-account.date-of-birth.over13-helper',
|
||||
defaultMessage: 'You must be over 13 years old to use Modrinth.',
|
||||
},
|
||||
under13HelperText: {
|
||||
id: 'auth.create-account.date-of-birth.under13-helper',
|
||||
defaultMessage: 'You cannot create an account at Modrinth unless you are 13 years old.',
|
||||
},
|
||||
ageRequirementWarningTitle: {
|
||||
id: 'auth.create-account.age-requirement.warning-title',
|
||||
defaultMessage: 'Age requirement',
|
||||
},
|
||||
infoPanelText: {
|
||||
id: 'auth.create-account.info-panel.text',
|
||||
defaultMessage:
|
||||
|
||||
@@ -79,13 +79,6 @@
|
||||
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 }">
|
||||
@@ -101,17 +94,11 @@
|
||||
</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()"
|
||||
:disabled="!emailModel || !passwordModel"
|
||||
@click="onContinueWithEmail()"
|
||||
>
|
||||
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
|
||||
</button>
|
||||
@@ -147,7 +134,6 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
@@ -156,7 +142,6 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
import { getAuthUrl } from '@/composables/auth.ts'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -172,10 +157,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
globals: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -184,29 +165,17 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subscribe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onToggleOtherOptions: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onCreateAccount: {
|
||||
onContinueWithEmail: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onSetCaptchaRef: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:email', 'update:password', 'update:token', 'update:subscribe'])
|
||||
const emit = defineEmits(['update:email', 'update:password'])
|
||||
|
||||
const emailModel = computed({
|
||||
get: () => props.email,
|
||||
@@ -218,16 +187,6 @@ const passwordModel = computed({
|
||||
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({
|
||||
@@ -239,10 +198,6 @@ const messages = defineMessages({
|
||||
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:
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<template>
|
||||
<SignUpView
|
||||
v-if="!isCreateAccountStep"
|
||||
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-continue-with-email="continueWithEmail"
|
||||
/>
|
||||
<CreateAccountView
|
||||
v-else
|
||||
v-model:date-of-birth="dateOfBirth"
|
||||
v-model:username="username"
|
||||
v-model:token="token"
|
||||
v-model:subscribe="subscribe"
|
||||
:globals="globals"
|
||||
:on-complete-sign-up="createAccount"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
@@ -24,6 +31,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import CreateAccountView from '@/components/ui/auth/CreateAccount.vue'
|
||||
import SignUpView from '@/components/ui/auth/SignUp.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
@@ -35,6 +43,14 @@ const messages = defineMessages({
|
||||
id: 'auth.sign-up.title',
|
||||
defaultMessage: 'Sign Up',
|
||||
},
|
||||
ageRequirementWarningTitle: {
|
||||
id: 'auth.sign-up.age-requirement.warning-title',
|
||||
defaultMessage: 'Age requirement',
|
||||
},
|
||||
under13HelperText: {
|
||||
id: 'auth.create-account.date-of-birth.under13-helper',
|
||||
defaultMessage: 'You cannot create an account at Modrinth unless you are 13 years old.',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
@@ -46,6 +62,7 @@ const route = useNativeRoute()
|
||||
|
||||
const redirectTarget = route.query.redirect
|
||||
const showOtherOptions = ref(false)
|
||||
const isCreateAccountStep = ref(false)
|
||||
|
||||
if (auth.value.user) {
|
||||
await navigateTo('/dashboard')
|
||||
@@ -73,12 +90,38 @@ const { data: globals } = useQuery({
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const dateOfBirth = ref('')
|
||||
const username = ref('')
|
||||
const token = ref('')
|
||||
const subscribe = ref(false)
|
||||
|
||||
async function continueWithEmail() {
|
||||
startLoading()
|
||||
try {
|
||||
const generatedUsername = generateUsernameFromEmail(email.value)
|
||||
|
||||
await client.labrinth.auth_v2.validateCreateAccount({
|
||||
username: generatedUsername,
|
||||
password: password.value,
|
||||
email: email.value,
|
||||
})
|
||||
|
||||
token.value = ''
|
||||
username.value = generatedUsername
|
||||
isCreateAccountStep.value = true
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function generateUsernameFromEmail(emailAddress) {
|
||||
const [localPart = '', domainPart = ''] = emailAddress.trim().toLowerCase().split('@')
|
||||
const sanitized = `${localPart}_${domainPart}`
|
||||
const [localPart = ''] = emailAddress.trim().toLowerCase().split('@')
|
||||
const sanitized = localPart
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
@@ -90,7 +133,7 @@ async function createAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
const res = await client.labrinth.auth_v2.createAccount({
|
||||
username: generateUsernameFromEmail(email.value),
|
||||
username: username.value.trim() || generateUsernameFromEmail(email.value),
|
||||
password: password.value,
|
||||
email: email.value,
|
||||
challenge: token.value,
|
||||
|
||||
@@ -57,6 +57,22 @@ export class LabrinthAuthV2Module extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email/password inputs for account creation without creating an account.
|
||||
*
|
||||
* @param data - Prospective account credentials
|
||||
*/
|
||||
public async validateCreateAccount(
|
||||
data: Labrinth.Auth.v2.ValidateCreateAccountRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/auth/create/validate`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new account from an OAuth callback flow state
|
||||
*
|
||||
|
||||
@@ -278,6 +278,12 @@ export namespace Labrinth {
|
||||
session: string
|
||||
}
|
||||
|
||||
export type ValidateCreateAccountRequest = {
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export type CreateOAuthAccountRequest = {
|
||||
username: string
|
||||
state: string
|
||||
|
||||
Reference in New Issue
Block a user