mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
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 commit7ea0635d86. * 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 commitcad6bd4f92. * 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 commitb9b03796e3. * 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:
co-authored by
tdgao
Truman Gao
Michael H.
Calum H.
Calum H.
Prospector
DeDiamondPro
parent
6fc741f7c0
commit
ef4044534f
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user