feat: update auth flow (#5790)

* Backend routes for choosing username in OAuth flow

* fix up oauth flow routes

* improve URL-related OAuth code

* Use user-provided callback addr instead of SELF_ADDR

* Revert "Use user-provided callback addr instead of SELF_ADDR"

This reverts commit 7ea0635d86.

* fix flow

* fix: backend response for create oauth account

* feat: new auth flow (#5840)

* update auth with new designs

* refactor: auth.js to auth.ts

* refactor: componentize auth pages

* fix: auth pages height

* feat: initial implementation of new sign-in oauth

* fix create account flow

* fix checkbox

* remove hard coded username

* implement create user validation endpoint and add more specific error responses

* feat: implement under 13 DOB guard and email/password validation route

* fix: TOCTOU issue

* refactor: pnpm prepr

* fix: make sure staging uses staging

* fix: hcaptcha styles

* fix: copy

* remove: auth/welcome page as its no longer used

* refactor: bring root page card styles into individual components and use tailwind

* fix: account settings modals to use new modal and fix lots of bad styles

* refactor: pnpm prepr

* feat: implement last signed in indicator

* fix: append number when generated name from email is taken

* refactor: pnpm prepr

* fix: last sign in badge color

* fix: qa issues

* refactor: pnpm prepr

* fix: hover effect on native date picker

* chore: temp staging undo

* Revert "chore: temp staging undo"

This reverts commit cad6bd4f92.

* feat: handle app create account

* fix: last signed in style

* fix: add initOnMounted for SSR race

* refactor: use typescript

* refactor: pnpm prepr

* refactor: use typescript for reset-password

* refactor: convert verify-email to use typescript

* refactor: convert authorize.vue to use typescript

* fix: authorize.vue error states

* feat: small style updates

* feat: implement date picker component

* feat: improve UX and styles for range select

* refactor: pnpm prepr

* fix: range select border styles

* feat: implement date picker component in create account

* feat: implement preserve date for date picker

* update rust toolchain

* increase recursion limit

* fix: date picker can be null

* fix: calculate age based on user's timezone

* fix: number input icons color

* fix: date picker icons

* feat: improve styles

* fix: add width on date

* fix: hover color bad on number input

* fix lints

* feat: add default date open view

* fmt

* fix: account.vue

* fix: remove default date to open 13 years ago

* fix: edit copy on info banner

* fix: cannot hover over project card tooltip items (#6071)

fix: cannot hover over project cards

* feat: improve add dependency flow (#6075)

* fix: shadow on nav

* feat: improve add dependency flow

* feat: update suggested dependency style

* feat: update dependency rows to use version number and update styles

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

* feat: improve dependency project and version search

* fix: open modrinth project links in the app (#6072)

* pin tanstack versions + set pnpm min age to 7 days

* squash commits

* fix: 2 factor auth enter code screen styles

* update copy

* update copy

* improve reset password

* feat: update sign in screen

* fix: unused import

* Merge branch 'main' into boris/dev-908-backend-changes

* Revert "Merge branch 'main' into boris/dev-908-backend-changes"

This reverts commit b9b03796e3.

* fix: add stroke

* feat: add passkey support (#6375)

* feat: add passkey backend

* feat: passkey frontend

* invalidate sessions on compromised passkey

* chore: run sqlx prepare

* fix: make passkey button use both collumns to prevent empty space

* fix: correctly verify max passkeys in finish route

* fix: use structs for response

* fix: add rp name default

* style: use web::Json

* fmt

* feat: improve manage passkeys UI

* fix copy

* pnpm prepr

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Michael H. <michael@iptables.sh>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: DeDiamondPro <67508414+DeDiamondPro@users.noreply.github.com>
This commit is contained in:
aecsocket
2026-06-25 20:36:19 +00:00
committed by GitHub
co-authored by tdgao Truman Gao Michael H. Calum H. Calum H. Prospector DeDiamondPro
parent 6fc741f7c0
commit ef4044534f
125 changed files with 5170 additions and 2828 deletions
@@ -0,0 +1,196 @@
<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 lang="ts">
import {
commonMessages,
defineMessages,
injectModrinthClient,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import type { LocationQueryValue } from 'vue-router'
import CreateAccountView from '@/components/ui/auth/CreateAccount.vue'
import { getLauncherRedirectUrl, promotePendingSignInOAuthProvider } from '@/composables/auth.ts'
interface AuthGlobalsResponse {
captcha_enabled?: boolean
[key: string]: unknown
}
interface ApiErrorShape {
data?: {
description?: string
}
}
const getQueryString = (
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
): string => {
const firstValue = Array.isArray(value) ? value[0] : value
return typeof firstValue === 'string' ? firstValue : ''
}
const getErrorMessage = (error: unknown): string => {
const apiError = error as ApiErrorShape
if (typeof apiError?.data?.description === 'string') {
return apiError.data.description
}
if (error instanceof Error) {
return error.message
}
return String(error)
}
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 : ''
})
const dateOfBirth = ref('')
const username = ref(defaultUsername.value)
const token = ref('')
const subscribe = ref(false)
const subtleLauncherRedirectUri = ref<string>()
const captcha = ref<{ reset?: () => void } | null>(null)
const setCaptchaRef = (captchaRef: unknown) => {
captcha.value = (captchaRef as { reset?: () => void } | null) ?? null
}
const { data: globals } = useQuery<AuthGlobalsResponse>({
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: getErrorMessage(err),
type: 'error',
})
captcha.value?.reset?.()
}
stopLoading()
}
async function finishSignIn(sessionToken?: string | null) {
if (route.query.launcher) {
let token = sessionToken
if (!token) {
token = auth.value.token
}
promotePendingSignInOAuthProvider()
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()
promotePendingSignInOAuthProvider()
}
if (route.query.redirect) {
const redirect = decodeURIComponent(getQueryString(route.query.redirect))
await navigateTo(redirect, {
replace: true,
})
} else {
await navigateTo('/dashboard')
}
}
</script>