update
This commit is contained in:
@@ -11,7 +11,6 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"axios": "^1.8.4",
|
||||
|
||||
@@ -269,7 +269,7 @@ const canTestUnsaved = computed(() => {
|
||||
// Define all possible events (aligned with AuditLogView's allActionTypes)
|
||||
const allNotificationEvents: NotificationEvent[] = [
|
||||
'LOGIN_SUCCESS', 'LOGIN_FAILURE', 'LOGOUT', 'PASSWORD_CHANGED', // Added LOGOUT, PASSWORD_CHANGED
|
||||
'2FA_ENABLED', '2FA_DISABLED', 'PASSKEY_REGISTERED', 'PASSKEY_DELETED', // Added 2FA, changed PASSKEY_ADDED
|
||||
'2FA_ENABLED', '2FA_DISABLED', // Added 2FA,
|
||||
'CONNECTION_CREATED', 'CONNECTION_UPDATED', 'CONNECTION_DELETED', 'CONNECTION_TESTED', // Changed _ADDED, added _TESTED
|
||||
'PROXY_CREATED', 'PROXY_UPDATED', 'PROXY_DELETED', // Changed _ADDED
|
||||
'TAG_CREATED', 'TAG_UPDATED', 'TAG_DELETED', // Changed _ADDED
|
||||
|
||||
@@ -497,8 +497,6 @@
|
||||
"PASSWORD_CHANGED": "Password Changed",
|
||||
"2FA_ENABLED": "2FA Enabled",
|
||||
"2FA_DISABLED": "2FA Disabled",
|
||||
"PASSKEY_REGISTERED": "Passkey Registered",
|
||||
"PASSKEY_DELETED": "Passkey Deleted",
|
||||
"CONNECTION_CREATED": "Connection Created",
|
||||
"CONNECTION_UPDATED": "Connection Updated",
|
||||
"CONNECTION_DELETED": "Connection Deleted",
|
||||
@@ -717,8 +715,6 @@
|
||||
"PASSWORD_CHANGED": "Password Changed",
|
||||
"2FA_ENABLED": "2FA Enabled",
|
||||
"2FA_DISABLED": "2FA Disabled",
|
||||
"PASSKEY_REGISTERED": "Passkey Registered",
|
||||
"PASSKEY_DELETED": "Passkey Deleted",
|
||||
"CONNECTION_CREATED": "Connection Created",
|
||||
"CONNECTION_UPDATED": "Connection Updated",
|
||||
"CONNECTION_DELETED": "Connection Deleted",
|
||||
|
||||
@@ -497,8 +497,6 @@
|
||||
"PASSWORD_CHANGED": "パスワード変更",
|
||||
"2FA_ENABLED": "2段階認証有効",
|
||||
"2FA_DISABLED": "2段階認証無効",
|
||||
"PASSKEY_REGISTERED": "Passkey 登録",
|
||||
"PASSKEY_DELETED": "Passkey 削除",
|
||||
"CONNECTION_CREATED": "接続作成",
|
||||
"CONNECTION_UPDATED": "接続更新",
|
||||
"CONNECTION_DELETED": "接続削除",
|
||||
@@ -720,8 +718,6 @@
|
||||
"PASSWORD_CHANGED": "パスワード変更",
|
||||
"2FA_ENABLED": "2段階認証有効",
|
||||
"2FA_DISABLED": "2段階認証無効",
|
||||
"PASSKEY_REGISTERED": "Passkey 登録",
|
||||
"PASSKEY_DELETED": "Passkey 削除",
|
||||
"CONNECTION_CREATED": "接続作成",
|
||||
"CONNECTION_UPDATED": "接続更新",
|
||||
"CONNECTION_DELETED": "接続削除",
|
||||
|
||||
@@ -497,8 +497,6 @@
|
||||
"PASSWORD_CHANGED": "密码已修改",
|
||||
"2FA_ENABLED": "两步验证已启用",
|
||||
"2FA_DISABLED": "两步验证已禁用",
|
||||
"PASSKEY_REGISTERED": "Passkey 已注册",
|
||||
"PASSKEY_DELETED": "Passkey 已删除",
|
||||
"CONNECTION_CREATED": "连接已创建",
|
||||
"CONNECTION_UPDATED": "连接已更新",
|
||||
"CONNECTION_DELETED": "连接已删除",
|
||||
@@ -720,8 +718,6 @@
|
||||
"PASSWORD_CHANGED": "密码已修改",
|
||||
"2FA_ENABLED": "两步验证已启用",
|
||||
"2FA_DISABLED": "两步验证已禁用",
|
||||
"PASSKEY_REGISTERED": "Passkey 已注册",
|
||||
"PASSKEY_DELETED": "Passkey 已删除",
|
||||
"CONNECTION_CREATED": "连接已创建",
|
||||
"CONNECTION_UPDATED": "连接已更新",
|
||||
"CONNECTION_DELETED": "连接已删除",
|
||||
|
||||
@@ -36,13 +36,7 @@ interface FullCaptchaSettings {
|
||||
recaptchaSecretKey?: string; // We won't use this in authStore
|
||||
}
|
||||
|
||||
// 新增:Passkey 信息接口 (根据后端返回调整)
|
||||
interface PasskeyInfo {
|
||||
id: number; // 数据库中的 ID,用于删除
|
||||
name?: string; // 用户设置的名称
|
||||
transports?: string; // JSON string of transports like ["internal", "usb"]
|
||||
created_at?: number; // Unix timestamp
|
||||
}
|
||||
// Removed PasskeyInfo interface
|
||||
|
||||
|
||||
// Auth Store State 接口
|
||||
@@ -59,9 +53,7 @@ interface AuthState {
|
||||
};
|
||||
needsSetup: boolean; // 新增:是否需要初始设置
|
||||
publicCaptchaConfig: PublicCaptchaConfig | null; // NEW: Public CAPTCHA config
|
||||
passkeys: PasskeyInfo[]; // 新增:存储 Passkey 列表
|
||||
passkeysLoading: boolean; // 新增:Passkey 列表加载状态
|
||||
passkeysError: string | null; // 新增:Passkey 列表错误状态
|
||||
// Removed Passkey state properties
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
@@ -74,9 +66,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
ipBlacklist: { entries: [], total: 0 }, // 初始化黑名单状态
|
||||
needsSetup: false, // 初始假设不需要设置
|
||||
publicCaptchaConfig: null, // NEW: Initialize CAPTCHA config as null
|
||||
passkeys: [], // 初始化 Passkey 列表为空
|
||||
passkeysLoading: false,
|
||||
passkeysError: null,
|
||||
// Removed Passkey state initialization
|
||||
}),
|
||||
getters: {
|
||||
// 可以添加一些 getter,例如获取用户名
|
||||
@@ -180,7 +170,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
// 清除本地状态
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.passkeys = []; // 登出时清空 Passkey 列表
|
||||
// Removed passkeys clear on logout
|
||||
console.log('已登出');
|
||||
// 登出后重定向到登录页
|
||||
await router.push({ name: 'Login' });
|
||||
@@ -210,7 +200,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.loginRequires2FA = false;
|
||||
this.passkeys = []; // 未认证时清空 Passkey 列表
|
||||
// Removed passkeys clear on unauthenticated
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果获取状态失败 (例如 session 过期),则认为未认证
|
||||
@@ -218,7 +208,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.loginRequires2FA = false;
|
||||
this.passkeys = []; // 失败时也清空 Passkey 列表
|
||||
// Removed passkeys clear on error
|
||||
// 可选:如果不是 401 错误,可以记录更详细的日志
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
@@ -353,111 +343,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
}
|
||||
},
|
||||
|
||||
// --- Passkey Actions ---
|
||||
/**
|
||||
* 获取当前用户的 Passkey 列表
|
||||
*/
|
||||
async fetchPasskeys() {
|
||||
if (!this.isAuthenticated) return; // 确保用户已登录
|
||||
this.passkeysLoading = true;
|
||||
this.passkeysError = null;
|
||||
try {
|
||||
const response = await apiClient.get<PasskeyInfo[]>('/auth/passkeys');
|
||||
this.passkeys = response.data;
|
||||
console.log('获取 Passkey 列表成功:', this.passkeys);
|
||||
} catch (err: any) {
|
||||
console.error('获取 Passkey 列表失败:', err);
|
||||
this.passkeysError = err.response?.data?.message || err.message || '获取 Passkey 列表时发生未知错误。';
|
||||
this.passkeys = []; // 出错时清空列表
|
||||
} finally {
|
||||
this.passkeysLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除指定的 Passkey
|
||||
* @param passkeyId 要删除的 Passkey 的 ID
|
||||
*/
|
||||
async deletePasskey(passkeyId: number) {
|
||||
if (!this.isAuthenticated) throw new Error('用户未登录');
|
||||
// 可以添加一个 loading 状态 specific to deletion if needed
|
||||
this.passkeysError = null; // Clear previous errors
|
||||
try {
|
||||
await apiClient.delete(`/auth/passkeys/${passkeyId}`);
|
||||
console.log(`Passkey ID ${passkeyId} 已删除`);
|
||||
// 从本地状态中移除
|
||||
this.passkeys = this.passkeys.filter(key => key.id !== passkeyId);
|
||||
return true; // Indicate success
|
||||
} catch (err: any) {
|
||||
console.error(`删除 Passkey ID ${passkeyId} 失败:`, err);
|
||||
this.passkeysError = err.response?.data?.message || err.message || '删除 Passkey 时发生未知错误。';
|
||||
// 抛出错误以便 UI 显示
|
||||
throw new Error(this.passkeysError ?? '删除 Passkey 时发生未知错误。');
|
||||
}
|
||||
},
|
||||
|
||||
// --- Passkey Authentication Actions ---
|
||||
/**
|
||||
* 从后端获取 Passkey 认证选项
|
||||
*/
|
||||
async getPasskeyAuthenticationOptions() {
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
// 调用后端 API 获取选项
|
||||
const response = await apiClient.post('/auth/passkey/authenticate-options');
|
||||
console.log('获取 Passkey 认证选项成功:', response.data);
|
||||
return response.data; // 返回选项给调用者 (LoginView)
|
||||
} catch (err: any) {
|
||||
console.error('获取 Passkey 认证选项失败:', err);
|
||||
this.error = err.response?.data?.message || err.message || '获取 Passkey 认证选项时发生未知错误。';
|
||||
// 返回 null 或抛出错误,让调用者知道失败了
|
||||
return null;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证 Passkey 认证响应并登录
|
||||
* @param authenticationResponse 从 @simplewebauthn/browser 获取的响应
|
||||
* @param rememberMe 用户是否勾选了“记住我”
|
||||
*/
|
||||
async verifyPasskeyAuthentication(authenticationResponse: any, rememberMe: boolean) {
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
// 调用后端 API 验证响应
|
||||
const response = await apiClient.post<{ message: string; user: UserInfo }>('/auth/passkey/verify-authentication', {
|
||||
authenticationResponse,
|
||||
rememberMe // 将 rememberMe 状态传递给后端
|
||||
});
|
||||
|
||||
// Passkey 认证和登录成功
|
||||
this.isAuthenticated = true;
|
||||
this.user = response.data.user;
|
||||
this.loginRequires2FA = false; // Passkey 登录通常不需要额外 2FA
|
||||
console.log('Passkey 登录成功:', this.user);
|
||||
|
||||
// 设置语言
|
||||
if (this.user?.language) {
|
||||
setLocale(this.user.language);
|
||||
}
|
||||
|
||||
// 跳转到工作区并刷新
|
||||
window.location.href = '/';
|
||||
return { success: true };
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Passkey 认证验证失败:', err);
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.error = err.response?.data?.message || err.message || 'Passkey 登录时发生未知错误。';
|
||||
return { success: false, error: this.error };
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
// --- Passkey Actions Removed ---
|
||||
},
|
||||
persist: true, // Revert to simple persistence to fix TS error for now
|
||||
});
|
||||
|
||||
@@ -9,8 +9,6 @@ export type AuditLogActionType =
|
||||
| 'PASSWORD_CHANGED'
|
||||
| '2FA_ENABLED'
|
||||
| '2FA_DISABLED'
|
||||
| 'PASSKEY_REGISTERED'
|
||||
| 'PASSKEY_DELETED'
|
||||
// Connections
|
||||
| 'CONNECTION_CREATED'
|
||||
| 'CONNECTION_UPDATED'
|
||||
|
||||
@@ -21,7 +21,7 @@ export type NotificationChannelType = 'webhook' | 'email' | 'telegram';
|
||||
// Align NotificationEvent with AuditLogActionType as requested
|
||||
export type NotificationEvent =
|
||||
| 'LOGIN_SUCCESS' | 'LOGIN_FAILURE' | 'LOGOUT' | 'PASSWORD_CHANGED'
|
||||
| '2FA_ENABLED' | '2FA_DISABLED' | 'PASSKEY_REGISTERED' | 'PASSKEY_DELETED'
|
||||
| '2FA_ENABLED' | '2FA_DISABLED'
|
||||
| 'CONNECTION_CREATED' | 'CONNECTION_UPDATED' | 'CONNECTION_DELETED' | 'CONNECTION_TESTED'
|
||||
| 'PROXY_CREATED' | 'PROXY_UPDATED' | 'PROXY_DELETED'
|
||||
| 'TAG_CREATED' | 'TAG_UPDATED' | 'TAG_DELETED'
|
||||
@@ -74,7 +74,7 @@ export type NotificationSettingData = Omit<NotificationSetting, 'id' | 'created_
|
||||
// Keep action types aligned with backend for potential filtering
|
||||
export type AuditLogActionType =
|
||||
| 'LOGIN_SUCCESS' | 'LOGIN_FAILURE' | 'LOGOUT' | 'PASSWORD_CHANGED'
|
||||
| '2FA_ENABLED' | '2FA_DISABLED' | 'PASSKEY_REGISTERED' | 'PASSKEY_DELETED'
|
||||
| '2FA_ENABLED' | '2FA_DISABLED'
|
||||
| 'CONNECTION_CREATED' | 'CONNECTION_UPDATED' | 'CONNECTION_DELETED' | 'CONNECTION_TESTED'
|
||||
| 'PROXY_CREATED' | 'PROXY_UPDATED' | 'PROXY_DELETED'
|
||||
| 'TAG_CREATED' | 'TAG_UPDATED' | 'TAG_DELETED'
|
||||
|
||||
@@ -118,7 +118,7 @@ const selectedActionType = ref<AuditLogActionType | ''>(''); // Allow empty stri
|
||||
// Define all possible action types for the dropdown
|
||||
const allActionTypes: AuditLogActionType[] = [
|
||||
'LOGIN_SUCCESS', 'LOGIN_FAILURE', 'LOGOUT', 'PASSWORD_CHANGED',
|
||||
'2FA_ENABLED', '2FA_DISABLED', 'PASSKEY_REGISTERED', 'PASSKEY_DELETED',
|
||||
'2FA_ENABLED', '2FA_DISABLED',
|
||||
'CONNECTION_CREATED', 'CONNECTION_UPDATED', 'CONNECTION_DELETED', 'CONNECTION_TESTED',
|
||||
'PROXY_CREATED', 'PROXY_UPDATED', 'PROXY_DELETED',
|
||||
'TAG_CREATED', 'TAG_UPDATED', 'TAG_DELETED',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { reactive, ref, onMounted } from 'vue'; // computed 不再直接使用,移除
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { startAuthentication } from '@simplewebauthn/browser'; // <-- 导入 Passkey 函数
|
||||
// Removed Passkey import: import { startAuthentication } from '@simplewebauthn/browser';
|
||||
import { useAuthStore } from '../stores/auth.store';
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import VueRecaptcha from 'vue3-recaptcha2'; // 使用默认导入
|
||||
@@ -98,46 +98,7 @@ onMounted(() => {
|
||||
authStore.fetchCaptchaConfig();
|
||||
});
|
||||
|
||||
// --- Passkey Login Handler ---
|
||||
const handlePasskeyLogin = async () => {
|
||||
authStore.clearError(); // 清除之前的错误
|
||||
captchaError.value = null; // 清除 CAPTCHA 错误
|
||||
try {
|
||||
// 1. 从后端获取认证选项 (包含 challenge)
|
||||
// 需要 authStore 中添加 getPasskeyAuthenticationOptions action
|
||||
const options = await authStore.getPasskeyAuthenticationOptions();
|
||||
if (!options) {
|
||||
// 错误已在 store action 中处理
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 使用浏览器 API 开始认证
|
||||
let authenticationResponse;
|
||||
try {
|
||||
authenticationResponse = await startAuthentication(options);
|
||||
} catch (err: any) {
|
||||
console.error('Passkey authentication failed (startAuthentication):', err);
|
||||
// 用户取消或浏览器不支持等情况
|
||||
if (err.name === 'NotAllowedError') {
|
||||
authStore.setError(t('login.error.passkeyCancelled'));
|
||||
} else {
|
||||
authStore.setError(t('login.error.passkeyFailed', { error: err.message || err.name || 'Unknown error' }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 将认证响应发送到后端进行验证
|
||||
// 需要 authStore 中添加 verifyPasskeyAuthentication action
|
||||
await authStore.verifyPasskeyAuthentication(authenticationResponse, rememberMe.value);
|
||||
// 成功后的重定向由 store action 处理
|
||||
// 失败会更新 error 状态并在模板中显示
|
||||
|
||||
} catch (err) {
|
||||
// Store action 中的错误已处理,这里无需额外操作
|
||||
console.error('Error during passkey login flow:', err);
|
||||
}
|
||||
};
|
||||
// --- End Passkey Login Handler ---
|
||||
// --- Passkey Login Handler Removed ---
|
||||
|
||||
</script>
|
||||
<template>
|
||||
@@ -237,15 +198,8 @@ const handlePasskeyLogin = async () => {
|
||||
{{ isLoading ? t('login.loggingIn') : (loginRequires2FA ? t('login.verifyButton') : t('login.loginButton')) }}
|
||||
</button>
|
||||
|
||||
<!-- Passkey Login Button -->
|
||||
<button type="button" @click="handlePasskeyLogin" :disabled="isLoading"
|
||||
class="w-full mt-3 py-3 px-4 bg-secondary text-secondary-foreground border border-border/50 rounded-lg text-base font-semibold cursor-pointer shadow-sm transition-colors duration-200 ease-in-out hover:bg-secondary/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary disabled:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-2 -mt-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 11c.828 0 1.5.672 1.5 1.5S12.828 14 12 14s-1.5-.672-1.5-1.5S11.172 11 12 11zm0-9a7 7 0 00-7 7c0 1.886.738 3.627 1.946 4.946.06.061.117.122.17.185l-.018.018-.002.002A9.5 9.5 0 0012 21.5a9.5 9.5 0 007.89-4.352l-.002-.002-.018-.018a6.965 6.965 0 001.946-4.946 7 7 0 00-7-7zm0 1.5a5.5 5.5 0 110 11 5.5 5.5 0 010-11z" />
|
||||
</svg>
|
||||
{{ t('login.passkeyLoginButton', '使用 Passkey 登录') }}
|
||||
</button>
|
||||
|
||||
<!-- Passkey Login Button Removed -->
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,52 +50,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<hr class="border-border/50">
|
||||
<!-- Passkey -->
|
||||
<div class="settings-section-content">
|
||||
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.passkey.title') }}</h3>
|
||||
<p class="text-sm text-text-secondary mb-4">{{ $t('settings.passkey.description') }}</p>
|
||||
<div class="mb-4">
|
||||
<label for="passkey-name" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.passkey.nameLabel') }}:</label>
|
||||
<input type="text" id="passkey-name" v-model="passkeyName" :placeholder="$t('settings.passkey.namePlaceholder')" required
|
||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<button @click="handleRegisterPasskey"
|
||||
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out text-sm font-medium">
|
||||
{{ $t('settings.passkey.registerButton') }}
|
||||
</button>
|
||||
<p v-if="passkeyMessage" class="text-sm text-success">{{ passkeyMessage }}</p>
|
||||
<p v-if="passkeyError" class="text-sm text-error">{{ passkeyError }}</p>
|
||||
</div>
|
||||
<!-- Passkey List -->
|
||||
<div class="mt-6 pt-4 border-t border-border/50">
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3">{{ $t('settings.passkey.registeredKeysTitle', '已注册的 Passkey') }}</h4>
|
||||
<!-- Loading State -->
|
||||
<div v-if="passkeysLoading" class="text-sm text-text-secondary italic">{{ $t('common.loading') }}</div>
|
||||
<!-- Error State -->
|
||||
<div v-else-if="passkeysError" class="text-sm text-error">{{ passkeysError }}</div>
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="passkeys.length === 0" class="text-sm text-text-secondary italic">{{ $t('settings.passkey.noKeysRegistered', '尚未注册任何 Passkey。') }}</div>
|
||||
<!-- List -->
|
||||
<ul v-else class="space-y-3">
|
||||
<li v-for="key in passkeys" :key="key.id" class="flex items-center justify-between p-3 border border-border rounded-md bg-header/30 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-foreground mr-2">{{ key.name || $t('settings.passkey.unnamedKey', '未命名密钥') }}</span>
|
||||
<span class="text-xs text-text-secondary">({{ $t('settings.passkey.registeredOn', '注册于') }}: {{ formatDate(key.created_at) }})</span>
|
||||
</div>
|
||||
<button
|
||||
@click="handleDeletePasskey(key.id)"
|
||||
:disabled="deletingPasskeyId === key.id"
|
||||
class="px-2 py-1 bg-error text-error-text rounded text-xs font-medium hover:bg-error/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-error disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out"
|
||||
>
|
||||
{{ deletingPasskeyId === key.id ? $t('common.deleting') : $t('common.delete') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="deletePasskeyError" class="mt-2 text-sm text-error">{{ deletePasskeyError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-border/50">
|
||||
<!-- Passkey Section Removed -->
|
||||
<!-- 2FA -->
|
||||
<div class="settings-section-content">
|
||||
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.twoFactor.title') }}</h3>
|
||||
@@ -564,7 +519,7 @@ import { storeToRefs } from 'pinia';
|
||||
import { availableLocales } from '../i18n'; // 导入可用语言列表
|
||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
||||
import { isAxiosError } from 'axios'; // 单独导入 isAxiosError
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
// Removed Passkey import: import { startRegistration } from '@simplewebauthn/browser';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -588,8 +543,7 @@ const {
|
||||
commandInputSyncTarget, // NEW: Import command input sync target getter
|
||||
} = storeToRefs(settingsStore);
|
||||
|
||||
// 从 authStore 获取 Passkey 相关状态
|
||||
const { passkeys, passkeysLoading, passkeysError } = storeToRefs(authStore);
|
||||
// Removed Passkey state import from authStore
|
||||
|
||||
|
||||
// --- Local state for forms ---
|
||||
@@ -663,9 +617,7 @@ const captchaForm = reactive<UpdateCaptchaSettingsDto>({ // Use reactive for the
|
||||
const captchaLoading = ref(false);
|
||||
const captchaMessage = ref('');
|
||||
const captchaSuccess = ref(false);
|
||||
// Passkey Deletion State
|
||||
const deletingPasskeyId = ref<number | null>(null);
|
||||
const deletePasskeyError = ref<string | null>(null);
|
||||
// Removed Passkey Deletion State
|
||||
|
||||
|
||||
// 提供一些常用的时区供选择
|
||||
@@ -893,70 +845,9 @@ const openStyleCustomizer = () => {
|
||||
appearanceStore.toggleStyleCustomizer(true);
|
||||
};
|
||||
|
||||
// --- Passkey state & methods ---
|
||||
const passkeyName = ref('');
|
||||
const passkeyMessage = ref<string | null>(null);
|
||||
const passkeyError = ref<string | null>(null);
|
||||
const handleRegisterPasskey = async () => {
|
||||
passkeyMessage.value = null;
|
||||
passkeyError.value = null;
|
||||
if (!passkeyName.value) {
|
||||
passkeyError.value = t('settings.passkey.error.nameRequired');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.log('[Passkey Register] 开始获取注册选项...');
|
||||
const optionsResponse = await apiClient.post('/auth/passkey/register-options'); // 使用 apiClient
|
||||
const options = optionsResponse.data;
|
||||
console.log('[Passkey Register] 获取到的注册选项:', JSON.stringify(options, null, 2)); // 记录选项
|
||||
// --- Passkey state & methods Removed ---
|
||||
|
||||
console.log('[Passkey Register] 调用 startRegistration...');
|
||||
let registrationResponse = await startRegistration(options);
|
||||
console.log('[Passkey Register] startRegistration 返回结果:', JSON.stringify(registrationResponse, null, 2)); // 记录响应
|
||||
|
||||
const verificationPayload = { ...registrationResponse, name: passkeyName.value };
|
||||
console.log('[Passkey Register] 调用验证接口,发送数据:', JSON.stringify(verificationPayload, null, 2)); // 记录发送的数据
|
||||
|
||||
// 将 startRegistration 返回的对象字段展开,与 name 一起作为请求体发送
|
||||
await apiClient.post('/auth/passkey/verify-registration', verificationPayload); // 使用 apiClient
|
||||
console.log('[Passkey Register] 验证接口调用成功。');
|
||||
|
||||
passkeyMessage.value = t('settings.passkey.success.registered');
|
||||
passkeyName.value = '';
|
||||
await authStore.fetchPasskeys(); // 注册成功后刷新列表
|
||||
} catch (error: any) {
|
||||
// 在 catch 块中记录更详细的错误信息
|
||||
console.error('[Passkey Register] 注册流程出错:', error);
|
||||
console.error('[Passkey Register] 错误详情:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); // 尝试记录错误对象的更多属性
|
||||
|
||||
if (error.name === 'NotAllowedError') {
|
||||
passkeyError.value = t('settings.passkey.error.cancelled');
|
||||
} else if (isAxiosError(error) && error.response) { // 使用导入的 isAxiosError
|
||||
passkeyError.value = t('settings.passkey.error.verificationFailed', { message: error.response.data.message || 'Server error' });
|
||||
} else {
|
||||
passkeyError.value = t('settings.passkey.error.genericRegistration', { message: error.message || t('settings.passkey.error.unknown') });
|
||||
}
|
||||
}
|
||||
};
|
||||
// 新增:处理 Passkey 删除
|
||||
const handleDeletePasskey = async (id: number) => {
|
||||
deletingPasskeyId.value = id;
|
||||
deletePasskeyError.value = null;
|
||||
if (confirm(t('settings.passkey.confirmDelete', '确定要删除这个 Passkey 吗?'))) { // 需要添加翻译
|
||||
try {
|
||||
await authStore.deletePasskey(id);
|
||||
// 列表会自动更新,因为 store action 修改了 state
|
||||
} catch (error: any) {
|
||||
console.error('删除 Passkey 失败:', error);
|
||||
deletePasskeyError.value = error.message || t('settings.passkey.error.deleteFailed', '删除 Passkey 失败'); // 需要添加翻译
|
||||
} finally {
|
||||
deletingPasskeyId.value = null;
|
||||
}
|
||||
} else {
|
||||
deletingPasskeyId.value = null;
|
||||
}
|
||||
};
|
||||
// 新增:格式化日期函数
|
||||
// --- Formatting function (kept in case other parts need it, can be removed if unused) ---
|
||||
const formatDate = (timestamp: number | undefined) => {
|
||||
if (!timestamp) return t('statusMonitor.notAvailable');
|
||||
try {
|
||||
@@ -1208,7 +1099,7 @@ onMounted(async () => {
|
||||
await checkTwoFactorStatus(); // Check 2FA status
|
||||
await fetchIpBlacklist(); // Fetch current blacklist entries
|
||||
await settingsStore.loadCaptchaSettings(); // <-- Load CAPTCHA settings
|
||||
await authStore.fetchPasskeys(); // <-- 获取 Passkey 列表
|
||||
// Removed fetchPasskeys call: await authStore.fetchPasskeys();
|
||||
// Initial settings (including language, whitelist, blacklist config) are loaded in main.ts via settingsStore.loadInitialSettings()
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user