feat: add passkey authentication

This commit is contained in:
Zhixuan Jiang
2026-07-26 09:50:28 -04:00
parent 2730c1c43b
commit cc62979aa7
37 changed files with 1797 additions and 8 deletions
+47 -2
View File
@@ -131,7 +131,7 @@
@open="showAgreementModal = true"
/>
<div v-if="showOAuthLogin" class="space-y-3 pt-1">
<div v-if="showPasskeyLogin || showOAuthLogin" class="space-y-3 pt-1">
<div class="flex items-center gap-3">
<div class="h-px flex-1 bg-gray-200 dark:bg-dark-700"></div>
<span class="text-xs text-gray-500 dark:text-dark-400">
@@ -140,6 +140,17 @@
<div class="h-px flex-1 bg-gray-200 dark:bg-dark-700"></div>
</div>
<button
v-if="showPasskeyLogin"
type="button"
class="btn btn-secondary w-full"
:disabled="authActionDisabled"
@click="handlePasskeyLogin"
>
<Icon name="key" size="md" class="mr-2" />
{{ passkeyLoading ? t('auth.passkeySigningIn') : t('auth.passkeySignIn') }}
</button>
<EmailOAuthButtons
:disabled="authActionDisabled"
:github-enabled="githubOAuthEnabled"
@@ -229,6 +240,7 @@ const appStore = useAppStore()
// ==================== State ====================
const isLoading = ref<boolean>(false)
const passkeyLoading = ref<boolean>(false)
const errorMessage = ref<string>('')
const showPassword = ref<boolean>(false)
const publicSettingsLoaded = ref<boolean>(false)
@@ -245,6 +257,7 @@ const oidcOAuthProviderName = ref<string>('OIDC')
const githubOAuthEnabled = ref<boolean>(false)
const googleOAuthEnabled = ref<boolean>(false)
const passwordResetEnabled = ref<boolean>(false)
const passkeyEnabled = ref<boolean>(false)
const loginAgreementEnabled = ref<boolean>(false)
const loginAgreementMode = ref<'modal' | 'checkbox' | string>('modal')
const loginAgreementUpdatedAt = ref<string>('')
@@ -283,7 +296,11 @@ const agreementGateActive = computed(
)
const authActionDisabled = computed(
() => isLoading.value || !publicSettingsLoaded.value || agreementGateActive.value
() => isLoading.value || passkeyLoading.value || !publicSettingsLoaded.value || agreementGateActive.value
)
const showPasskeyLogin = computed(
() => passkeyEnabled.value && typeof window.PublicKeyCredential !== 'undefined'
)
const showOAuthLogin = computed(
@@ -328,6 +345,7 @@ onMounted(async () => {
googleOAuthEnabled.value = settings.google_oauth_enabled
backendModeEnabled.value = settings.backend_mode_enabled
passwordResetEnabled.value = settings.password_reset_enabled
passkeyEnabled.value = settings.passkey_enabled === true
applyLoginAgreementSettings(settings)
} catch (error) {
console.error('Failed to load public settings:', error)
@@ -516,6 +534,33 @@ async function handleLogin(): Promise<void> {
}
}
async function handlePasskeyLogin(): Promise<void> {
if (agreementGateActive.value) {
appStore.showWarning(t('legal.loginAgreementPrompt.loginRequiredWarning'))
if (loginAgreementMode.value !== 'checkbox') {
showAgreementModal.value = true
}
return
}
passkeyLoading.value = true
try {
await authStore.loginWithPasskey()
clearAllAffiliateReferralCodes()
appStore.showSuccess(t('auth.loginSuccess'))
const redirectTo = (router.currentRoute.value.query.redirect as string) || '/dashboard'
await router.push(redirectTo)
} catch (error: unknown) {
const fallback = error instanceof DOMException && error.name === 'NotAllowedError'
? t('auth.passkeyCancelled')
: t('auth.passkeyFailed')
errorMessage.value = extractI18nErrorMessage(error, t, 'auth.errors', fallback)
appStore.showError(errorMessage.value)
} finally {
passkeyLoading.value = false
}
}
// ==================== 2FA Handlers ====================
async function handle2FAVerify(code: string): Promise<void> {
+4
View File
@@ -44,6 +44,7 @@
/>
<ProfileTotpCard />
<ProfilePasskeyCard :enabled="passkeyEnabled" />
</div>
</AppLayout>
</template>
@@ -57,6 +58,7 @@ import ProfileBalanceNotifyCard from '@/components/user/profile/ProfileBalanceNo
import ProfileInfoCard from '@/components/user/profile/ProfileInfoCard.vue'
import ProfilePasswordForm from '@/components/user/profile/ProfilePasswordForm.vue'
import ProfileTotpCard from '@/components/user/profile/ProfileTotpCard.vue'
import ProfilePasskeyCard from '@/components/user/profile/ProfilePasskeyCard.vue'
import { isWeChatWebOAuthEnabled } from '@/api/auth'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
@@ -76,6 +78,7 @@ const wechatOAuthOpenEnabled = ref<boolean | undefined>(undefined)
const wechatOAuthMPEnabled = ref<boolean | undefined>(undefined)
const oidcOAuthEnabled = ref(false)
const oidcOAuthProviderName = ref('OIDC')
const passkeyEnabled = ref(false)
onMounted(async () => {
const profileRefresh = authStore.refreshUser().catch((error) => {
@@ -101,6 +104,7 @@ onMounted(async () => {
: undefined
oidcOAuthEnabled.value = settings.oidc_oauth_enabled ?? false
oidcOAuthProviderName.value = settings.oidc_oauth_provider_name || 'OIDC'
passkeyEnabled.value = settings.passkey_enabled === true
})
.catch((error) => {
console.error('Failed to load settings:', error)