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
+152 -131
View File
@@ -9,24 +9,31 @@
:has-to-type="true"
@proceed="deleteAccount"
/>
<Modal
<NewModal
ref="changeEmailModal"
width="500px"
:header="`${auth.user.email ? formatMessage(messages.changeEmailHeaderChange) : formatMessage(messages.changeEmailHeaderAdd)}`"
>
<div class="universal-modal">
<p>{{ formatMessage(messages.emailNotPublicNotice) }}</p>
<label for="email-input">
<span class="label__title">{{ formatMessage(messages.emailAddressLabel) }}</span>
</label>
<StyledInput
id="email-input"
v-model="email"
:maxlength="2048"
type="email"
:placeholder="formatMessage(messages.emailAddressPlaceholder)"
@keyup.enter="saveEmail()"
/>
<div class="input-group push-right">
<div class="flex flex-col gap-6">
<Admonition type="info">
{{ formatMessage(messages.emailNotPublicNotice) }}
</Admonition>
<div class="flex flex-col gap-2.5">
<label for="email-input">
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.emailAddressLabel)
}}</span>
</label>
<StyledInput
id="email-input"
v-model="email"
:maxlength="2048"
type="email"
:placeholder="formatMessage(messages.emailAddressPlaceholder)"
@keyup.enter="saveEmail()"
/>
</div>
<div class="flex justify-end gap-2.5">
<ButtonStyled>
<button @click="$refs.changeEmailModal.hide()">
<XIcon />
@@ -41,9 +48,10 @@
</ButtonStyled>
</div>
</div>
</Modal>
<Modal
</NewModal>
<NewModal
ref="managePasswordModal"
width="600px"
:header="`${
removePasswordMode
? formatMessage(messages.passwordHeaderRemove)
@@ -52,61 +60,75 @@
: formatMessage(messages.passwordHeaderAdd)
}`"
>
<div class="universal-modal">
<ul
v-if="newPassword !== confirmNewPassword && confirmNewPassword.length > 0"
class="known-errors"
>
<li>{{ formatMessage(messages.passwordsDoNotMatchError) }}</li>
</ul>
<label v-if="removePasswordMode" for="old-password">
<span class="label__title">{{ formatMessage(messages.confirmPasswordLabel) }}</span>
<span class="label__description">{{
formatMessage(messages.confirmPasswordDescription)
}}</span>
</label>
<label v-else-if="auth.user.has_password" for="old-password">
<span class="label__title">{{ formatMessage(messages.oldPasswordLabel) }}</span>
</label>
<StyledInput
v-if="auth.user.has_password"
id="old-password"
v-model="oldPassword"
:maxlength="2048"
type="password"
autocomplete="current-password"
:placeholder="
removePasswordMode
? formatMessage(messages.confirmPasswordPlaceholder)
: formatMessage(messages.oldPasswordPlaceholder)
"
/>
<template v-if="!removePasswordMode">
<label for="new-password"
><span class="label__title">{{ formatMessage(messages.newPasswordLabel) }}</span></label
>
<StyledInput
id="new-password"
v-model="newPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(messages.newPasswordPlaceholder)"
/>
<label for="confirm-new-password">
<span class="label__title">{{ formatMessage(messages.confirmNewPasswordLabel) }}</span>
<div class="flex flex-col gap-6">
<div v-if="auth.user.has_password" class="flex flex-col gap-2.5">
<label v-if="removePasswordMode" for="old-password">
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.confirmPasswordLabel)
}}</span>
</label>
<label v-else for="old-password">
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.oldPasswordLabel)
}}</span>
</label>
<StyledInput
id="confirm-new-password"
v-model="confirmNewPassword"
id="old-password"
v-model="oldPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(messages.confirmNewPasswordPlaceholder)"
autocomplete="current-password"
:placeholder="
removePasswordMode
? formatMessage(messages.confirmPasswordPlaceholder)
: formatMessage(messages.oldPasswordPlaceholder)
"
/>
<span v-if="removePasswordMode" class="label__description">{{
formatMessage(messages.confirmPasswordDescription)
}}</span>
</div>
<template v-if="!removePasswordMode">
<div class="flex flex-col gap-2.5">
<label for="new-password"
><span class="text-md font-semibold text-contrast">{{
formatMessage(messages.newPasswordLabel)
}}</span></label
>
<StyledInput
id="new-password"
v-model="newPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(messages.newPasswordPlaceholder)"
/>
</div>
<div class="flex flex-col gap-2.5">
<label for="confirm-new-password">
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.confirmNewPasswordLabel)
}}</span>
</label>
<StyledInput
id="confirm-new-password"
v-model="confirmNewPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(messages.confirmNewPasswordPlaceholder)"
/>
<div
v-if="newPassword !== confirmNewPassword && confirmNewPassword.length > 0"
class="known-errors"
>
{{ formatMessage(messages.passwordsDoNotMatchError) }}
</div>
</div>
</template>
<p></p>
<div class="input-group push-right">
<div class="flex justify-end gap-2.5">
<ButtonStyled>
<button @click="$refs.managePasswordModal.hide()">
<XIcon />
@@ -147,9 +169,10 @@
</template>
</div>
</div>
</Modal>
<Modal
</NewModal>
<NewModal
ref="manageTwoFactorModal"
width="500px"
:header="
formatMessage(
auth.user.has_totp && twoFactorStep === 0
@@ -158,10 +181,12 @@
)
"
>
<div class="universal-modal">
<div class="flex flex-col gap-6">
<template v-if="auth.user.has_totp && twoFactorStep === 0">
<label for="two-factor-code">
<span class="label__title">{{ formatMessage(messages.twoFactorEnterCodeLabel) }}</span>
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.twoFactorEnterCodeLabel)
}}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorEnterCodeDescription)
}}</span>
@@ -173,10 +198,10 @@
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
@keyup.enter="removeTwoFactor()"
/>
<p v-if="twoFactorIncorrect" class="known-errors">
<p v-if="twoFactorIncorrect" class="known-errors m-0">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
<div class="input-group push-right">
<div class="flex justify-end gap-2.5">
<ButtonStyled>
<button @click="$refs.manageTwoFactorModal.hide()">
<XIcon />
@@ -193,8 +218,8 @@
</template>
<template v-else>
<template v-if="twoFactorStep === 0">
<p>{{ formatMessage(messages.twoFactorSetupIntro) }}</p>
<p>
<p class="m-0">{{ formatMessage(messages.twoFactorSetupIntro) }}</p>
<p class="m-0">
<IntlFormatted :message-id="messages.twoFactorSetupScan">
<template #authy-link="{ children }">
<a href="https://authy.com/" target="_blank" rel="noreferrer">
@@ -221,14 +246,14 @@
:margin="2"
level="H"
/>
<p>
<p class="m-0">
{{ formatMessage(messages.twoFactorManualSecretPrefix) }}
<strong>{{ twoFactorSecret }}</strong>
</p>
</template>
<template v-if="twoFactorStep === 1">
<label for="verify-code">
<span class="label__title">{{
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.twoFactorVerifyCodeLabel)
}}</span>
<span class="label__description">{{
@@ -243,18 +268,18 @@
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
@keyup.enter="verifyTwoFactorCode()"
/>
<p v-if="twoFactorIncorrect" class="known-errors">
<p v-if="twoFactorIncorrect" class="known-errors m-0">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
</template>
<template v-if="twoFactorStep === 2">
<p>{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
<p>{{ formatMessage(messages.twoFactorBackupCodesSingleUse) }}</p>
<p class="m-0">{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
<p class="m-0">{{ formatMessage(messages.twoFactorBackupCodesSingleUse) }}</p>
<ul>
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
</ul>
</template>
<div class="input-group push-right">
<div class="flex justify-end gap-2.5">
<ButtonStyled v-if="twoFactorStep === 1">
<button @click="twoFactorStep = 0">
<LeftArrowIcon />
@@ -282,38 +307,34 @@
</div>
</template>
</div>
</Modal>
<Modal ref="manageProvidersModal" :header="formatMessage(messages.manageProvidersModalHeader)">
<div class="universal-modal">
<div class="table">
<div class="table-head table-row">
<div class="table-text table-cell">
{{ formatMessage(messages.providersTableProvider) }}
</div>
<div class="table-text table-cell">
{{ formatMessage(messages.providersTableActions) }}
</div>
</div>
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
<div class="table-text table-cell">
<span><component :is="provider.icon" /> {{ provider.display }}</span>
</div>
<div class="table-text manage table-cell">
<ButtonStyled v-if="auth.user.auth_providers.includes(provider.id)">
<button @click="handleRemoveAuthProvider(provider.id)">
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else>
<a :href="`${getAuthUrl(provider.id, '/settings/account')}&token=${auth.token}`">
<ExternalIcon /> {{ formatMessage(messages.providerAddButton) }}
</a>
</ButtonStyled>
</div>
</div>
</div>
<p></p>
<div class="input-group push-right">
</NewModal>
<NewModal
ref="manageProvidersModal"
width="500px"
:header="formatMessage(messages.manageProvidersModalHeader)"
>
<div class="flex flex-col gap-4">
<Table :columns="manageProvidersColumns" :data="authProviders" row-key="id">
<template #cell-provider="{ row }">
<span class="inline-flex items-center gap-1.5">
<component :is="row.icon" class="size-5" />
{{ row.display }}
</span>
</template>
<template #cell-actions="{ row }">
<ButtonStyled v-if="auth.user.auth_providers.includes(row.id)">
<button class="!w-full" @click="handleRemoveAuthProvider(row.id)">
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else>
<a :href="`${getAuthUrl(row.id, '/settings/account')}&token=${auth.token}`">
<ExternalIcon /> {{ formatMessage(messages.providerAddButton) }}
</a>
</ButtonStyled>
</template>
</Table>
<div class="self-end">
<ButtonStyled>
<button @click="$refs.manageProvidersModal.hide()">
<XIcon />
@@ -322,7 +343,7 @@
</ButtonStyled>
</div>
</div>
</Modal>
</NewModal>
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(messages.accountSecurityTitle) }}</h2>
@@ -419,6 +440,7 @@
</ButtonStyled>
</div>
</div>
<PasskeySettings />
</section>
<section id="data-export" class="universal-card">
@@ -471,13 +493,16 @@ import {
XIcon,
} from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
commonMessages,
ConfirmModal,
defineMessages,
injectNotificationManager,
IntlFormatted,
NewModal,
StyledInput,
Table,
useVIntl,
} from '@modrinth/ui'
import KeyIcon from 'assets/icons/auth/key.svg'
@@ -489,8 +514,8 @@ import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
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 PasskeySettings from '~/components/ui/auth/PasskeySettings.vue'
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.ts'
definePageMeta({
middleware: 'auth',
@@ -957,6 +982,18 @@ const authProviders = [
icon: GoogleIcon,
},
]
const manageProvidersColumns = computed(() => [
{
key: 'provider',
label: formatMessage(messages.providersTableProvider),
},
{
key: 'actions',
label: formatMessage(messages.providersTableActions),
align: 'right',
width: '10rem',
},
])
async function deleteAccount() {
startLoading()
@@ -1010,20 +1047,4 @@ canvas {
margin: 0 auto;
border-radius: var(--size-rounded-card);
}
.table-row {
grid-template-columns: 1fr 10rem;
span {
display: flex;
align-items: center;
margin: auto 0;
svg {
width: 1.25rem;
height: 1.25rem;
margin-right: 0.35rem;
}
}
}
</style>