feat(auth): 实现 TOTP 双因素认证功能

新增功能:
- 支持 Google Authenticator 等应用进行 TOTP 二次验证
- 用户可在个人设置中启用/禁用 2FA
- 登录时支持 TOTP 验证流程
- 管理后台可全局开关 TOTP 功能

安全增强:
- TOTP 密钥使用 AES-256-GCM 加密存储
- 添加 TOTP_ENCRYPTION_KEY 配置项,必须手动配置才能启用功能
- 防止服务重启导致加密密钥变更使用户无法登录
- 验证失败次数限制,防止暴力破解

配置说明:
- Docker 部署:在 .env 中设置 TOTP_ENCRYPTION_KEY
- 非 Docker 部署:在 config.yaml 中设置 totp.encryption_key
- 生成密钥命令:openssl rand -hex 32
This commit is contained in:
shaw
2026-01-26 08:45:43 +08:00
parent 74e05b83ea
commit 1245f07a2d
60 changed files with 4140 additions and 350 deletions

View File

@@ -5,8 +5,8 @@
import { defineStore } from 'pinia'
import { ref, computed, readonly } from 'vue'
import { authAPI } from '@/api'
import type { User, LoginRequest, RegisterRequest } from '@/types'
import { authAPI, isTotp2FARequired, type LoginResponse } from '@/api'
import type { User, LoginRequest, RegisterRequest, AuthResponse } from '@/types'
const AUTH_TOKEN_KEY = 'auth_token'
const AUTH_USER_KEY = 'auth_user'
@@ -91,32 +91,23 @@ export const useAuthStore = defineStore('auth', () => {
/**
* User login
* @param credentials - Login credentials (username and password)
* @returns Promise resolving to the authenticated user
* @param credentials - Login credentials (email and password)
* @returns Promise resolving to the login response (may require 2FA)
* @throws Error if login fails
*/
async function login(credentials: LoginRequest): Promise<User> {
async function login(credentials: LoginRequest): Promise<LoginResponse> {
try {
const response = await authAPI.login(credentials)
// Store token and user
token.value = response.access_token
// Extract run_mode if present
if (response.user.run_mode) {
runMode.value = response.user.run_mode
// If 2FA is required, return the response without setting auth state
if (isTotp2FARequired(response)) {
return response
}
const { run_mode: _run_mode, ...userData } = response.user
user.value = userData
// Persist to localStorage
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
// Set auth state from the response
setAuthFromResponse(response)
// Start auto-refresh interval
startAutoRefresh()
return userData
return response
} catch (error) {
// Clear any partial state on error
clearAuth()
@@ -124,6 +115,47 @@ export const useAuthStore = defineStore('auth', () => {
}
}
/**
* Complete login with 2FA code
* @param tempToken - Temporary token from initial login
* @param totpCode - 6-digit TOTP code
* @returns Promise resolving to the authenticated user
* @throws Error if 2FA verification fails
*/
async function login2FA(tempToken: string, totpCode: string): Promise<User> {
try {
const response = await authAPI.login2FA({ temp_token: tempToken, totp_code: totpCode })
setAuthFromResponse(response)
return user.value!
} catch (error) {
clearAuth()
throw error
}
}
/**
* Set auth state from an AuthResponse
* Internal helper function
*/
function setAuthFromResponse(response: AuthResponse): void {
// Store token and user
token.value = response.access_token
// Extract run_mode if present
if (response.user.run_mode) {
runMode.value = response.user.run_mode
}
const { run_mode: _run_mode, ...userData } = response.user
user.value = userData
// Persist to localStorage
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
// Start auto-refresh interval
startAutoRefresh()
}
/**
* User registration
* @param userData - Registration data (username, email, password)
@@ -253,6 +285,7 @@ export const useAuthStore = defineStore('auth', () => {
// Actions
login,
login2FA,
register,
setToken,
logout,