feat: 实现 2FA (TOTP) 的设置、验证和禁用流程

This commit is contained in:
Baobhan Sith
2025-04-15 11:49:28 +08:00
parent ffb772546d
commit 171baec830
9 changed files with 1071 additions and 97 deletions
+86 -14
View File
@@ -2,10 +2,11 @@ import { defineStore } from 'pinia';
import axios from 'axios';
import router from '../router'; // 引入 router 用于重定向
// 用户信息接口 (不含敏感信息)
// 扩展的用户信息接口,包含 2FA 状态
interface UserInfo {
id: number;
username: string;
isTwoFactorEnabled?: boolean; // 后端 /status 接口会返回这个
}
// Auth Store State 接口
@@ -14,6 +15,7 @@ interface AuthState {
user: UserInfo | null;
isLoading: boolean;
error: string | null;
loginRequires2FA: boolean; // 新增状态:标记登录是否需要 2FA
}
export const useAuthStore = defineStore('auth', {
@@ -22,6 +24,7 @@ export const useAuthStore = defineStore('auth', {
user: null,
isLoading: false,
error: null,
loginRequires2FA: false, // 初始为不需要
}),
getters: {
// 可以添加一些 getter,例如获取用户名
@@ -32,32 +35,74 @@ export const useAuthStore = defineStore('auth', {
async login(credentials: { username: string; password: string }) {
this.isLoading = true;
this.error = null;
this.loginRequires2FA = false; // 重置 2FA 状态
try {
const response = await axios.post<{ message: string; user: UserInfo }>('/api/v1/auth/login', credentials);
// 登录成功
this.isAuthenticated = true;
this.user = response.data.user;
console.log('登录成功:', this.user);
// 登录成功后重定向到连接管理页面 (或仪表盘)
await router.push({ name: 'Connections' }); // 使用 await 确保导航完成
return true;
// 后端可能返回 user 或 requiresTwoFactor
const response = await axios.post<{ message: string; user?: UserInfo; requiresTwoFactor?: boolean }>('/api/v1/auth/login', credentials);
if (response.data.requiresTwoFactor) {
// 需要 2FA 验证
console.log('登录需要 2FA 验证');
this.loginRequires2FA = true;
// 不设置 isAuthenticated 和 user,等待 2FA 验证
return { requiresTwoFactor: true }; // 返回特殊状态给调用者
} else if (response.data.user) {
// 登录成功 (无 2FA)
this.isAuthenticated = true;
this.user = response.data.user;
console.log('登录成功 (无 2FA):', this.user);
await router.push({ name: 'Connections' });
return { success: true };
} else {
// 不应该发生,但作为防御性编程
throw new Error('登录响应无效');
}
} catch (err: any) {
console.error('登录失败:', err);
this.isAuthenticated = false;
this.user = null;
this.loginRequires2FA = false;
this.error = err.response?.data?.message || err.message || '登录时发生未知错误。';
return false;
return { success: false, error: this.error };
} finally {
this.isLoading = false;
}
},
// 登 Action (占位符)
async logout() {
// 登录时的 2FA 验证 Action
async verifyLogin2FA(token: string) {
if (!this.loginRequires2FA) {
throw new Error('当前登录流程不需要 2FA 验证。');
}
this.isLoading = true;
this.error = null;
try {
// TODO: 调用后端的登出 API (如果需要)
const response = await axios.post<{ message: string; user: UserInfo }>('/api/v1/auth/login/2fa', { token });
// 2FA 验证成功
this.isAuthenticated = true;
this.user = response.data.user;
this.loginRequires2FA = false; // 重置状态
console.log('2FA 验证成功,登录完成:', this.user);
await router.push({ name: 'Connections' });
return { success: true };
} catch (err: any) {
console.error('2FA 验证失败:', err);
// 不清除 isAuthenticated 或 user,因为用户可能只是输错了验证码
this.error = err.response?.data?.message || err.message || '2FA 验证时发生未知错误。';
return { success: false, error: this.error };
} finally {
this.isLoading = false;
}
},
// 登出 Action
async logout() {
this.isLoading = true;
this.error = null;
this.loginRequires2FA = false; // 重置 2FA 状态
try {
// TODO: 调用后端的登出 API
// await axios.post('/api/v1/auth/logout');
// 清除本地状态
@@ -101,7 +146,34 @@ export const useAuthStore = defineStore('auth', {
// }
// }
// }
// 新增:检查并更新认证状态 Action
async checkAuthStatus() {
this.isLoading = true;
try {
const response = await axios.get<{ isAuthenticated: boolean; user: UserInfo }>('/api/v1/auth/status');
if (response.data.isAuthenticated && response.data.user) {
this.isAuthenticated = true;
this.user = response.data.user; // 更新用户信息,包含 isTwoFactorEnabled
this.loginRequires2FA = false; // 确保重置
console.log('认证状态已更新:', this.user);
} else {
this.isAuthenticated = false;
this.user = null;
this.loginRequires2FA = false;
}
} catch (error: any) {
// 如果获取状态失败 (例如 session 过期),则认为未认证
console.warn('检查认证状态失败:', error.response?.data?.message || error.message);
this.isAuthenticated = false;
this.user = null;
this.loginRequires2FA = false;
// 可选:如果不是 401 错误,可以记录更详细的日志
} finally {
this.isLoading = false;
}
},
// 修改密码 Action
async changePassword(currentPassword: string, newPassword: string) {
if (!this.isAuthenticated) {