feat: 添加 passkey 登录功能

This commit is contained in:
Baobhan Sith
2025-05-08 14:13:32 +08:00
parent 56dcbc33e0
commit bc4ae93d7d
20 changed files with 1347 additions and 159 deletions
+50 -4
View File
@@ -2,7 +2,7 @@
import { reactive, ref, onMounted } from 'vue'; // computed 不再直接使用,移除
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
// Removed Passkey import: import { startAuthentication } from '@simplewebauthn/browser';
import { startAuthentication } from '@simplewebauthn/browser';
import { useAuthStore } from '../stores/auth.store';
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import VueRecaptcha from 'vue3-recaptcha2'; // 使用默认导入
@@ -98,7 +98,48 @@ onMounted(() => {
authStore.fetchCaptchaConfig();
});
// --- Passkey Login Handler Removed ---
// --- Passkey Login Handler ---
const handlePasskeyLogin = async () => {
// TODO: Implement Passkey login logic
// 1. Get username (assume it's available in credentials.username for now)
if (!credentials.username) {
// TODO: Handle missing username, maybe show an error
alert(t('login.error.usernameRequiredForPasskey'));
return;
}
try {
isLoading.value = true;
error.value = null; // Clear previous errors
// Step 1: Get authentication options from the server
const optionsResponse = await fetch('/api/v1/auth/passkey/authentication-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: credentials.username }),
});
if (!optionsResponse.ok) {
const errData = await optionsResponse.json();
throw new Error(errData.message || t('login.error.passkeyAuthOptionsFailed'));
}
const authOptions = await optionsResponse.json();
// Step 2: Use WebAuthn API to authenticate
const authenticationResult = await startAuthentication(authOptions);
// Step 3: Send authentication result to the server
await authStore.loginWithPasskey(credentials.username, authenticationResult);
} catch (err: any) {
console.error('Passkey login error:', err);
error.value = err.message || t('login.error.passkeyAuthFailed');
// Potentially reset CAPTCHA if it was involved, though typically not for passkey flows directly
// if (publicCaptchaConfig.value?.enabled) {
// resetCaptchaWidget();
// }
} finally {
isLoading.value = false;
}
};
</script>
<template>
@@ -198,8 +239,13 @@ onMounted(() => {
{{ isLoading ? t('login.loggingIn') : (loginRequires2FA ? t('login.verifyButton') : t('login.loginButton')) }}
</button>
<!-- Passkey Login Button Removed -->
<!-- Passkey Login Button -->
<div class="mt-4 text-center">
<button type="button" @click="handlePasskeyLogin" :disabled="isLoading"
class="w-full py-3 px-4 bg-secondary text-white border-none rounded-lg text-base font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-secondary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-70">
{{ isLoading ? t('login.loggingIn') : t('login.loginWithPasskey') }}
</button>
</div>
</form>
</div>
</div>