From 1f3631539b8156c7b8e7658f46f02eb8435934e6 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:38:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20IP=20=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E8=AE=BE=E7=BD=AE=E7=9A=84=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E5=8F=8A=E5=90=8E=E7=AB=AF=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dataSources.xml | 2 +- .../src/auth/ipWhitelist.middleware.ts | 108 ++++++++ packages/backend/src/index.ts | 4 + packages/backend/src/migrations.ts | 234 ++++-------------- .../src/repositories/settings.repository.ts | 9 +- .../backend/src/services/settings.service.ts | 25 ++ packages/frontend/src/locales/en.json | 14 ++ packages/frontend/src/locales/zh.json | 14 ++ packages/frontend/src/views/SettingsView.vue | 85 ++++++- 9 files changed, 303 insertions(+), 192 deletions(-) create mode 100644 packages/backend/src/auth/ipWhitelist.middleware.ts diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index a7970d3..d62a92e 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,7 +1,7 @@ - + sqlite.xerial true org.sqlite.JDBC diff --git a/packages/backend/src/auth/ipWhitelist.middleware.ts b/packages/backend/src/auth/ipWhitelist.middleware.ts new file mode 100644 index 0000000..12b390e --- /dev/null +++ b/packages/backend/src/auth/ipWhitelist.middleware.ts @@ -0,0 +1,108 @@ +import { Request, Response, NextFunction } from 'express'; +import ipaddr from 'ipaddr.js'; +import { settingsService } from '../services/settings.service'; + +const IP_WHITELIST_SETTING_KEY = 'ipWhitelist'; + +// 本地开发环境的 IP 地址列表 +const LOCAL_IPS = [ + '127.0.0.1', // IPv4 本地回环 + '::1', // IPv6 本地回环 + 'localhost' // 本地主机名 +]; + +/** + * IP 白名单中间件 + * 检查请求来源 IP 是否在设置中定义的白名单内。 + * 白名单支持 IPv4, IPv6 地址以及 CIDR 范围。 + * 如果白名单未设置或为空,则允许所有 IP。 + * 本地开发环境的 IP 地址始终允许访问。 + */ +export const ipWhitelistMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + // 获取请求 IP 地址 + const requestIpString = req.ip || req.socket.remoteAddress; + + if (!requestIpString) { + console.warn('无法获取请求 IP 地址,已拒绝访问。'); + return res.status(403).json({ message: '禁止访问:无法识别来源 IP。' }); + } + + // 检查是否是本地开发环境的 IP + if (LOCAL_IPS.includes(requestIpString)) { + console.log(`允许来自本地开发环境 (${requestIpString}) 的访问。`); + return next(); + } + + const whitelistString = await settingsService.getSetting(IP_WHITELIST_SETTING_KEY); + + // 如果白名单未设置或为空字符串,则允许所有请求 + if (!whitelistString || whitelistString.trim() === '') { + return next(); + } + + // 解析白名单字符串 (假设以换行符或逗号分隔) + const whitelistEntries = whitelistString + .split(/[\n,]+/) // 按换行符或逗号分割 + .map(entry => entry.trim()) // 去除首尾空格 + .filter(entry => entry.length > 0); // 过滤空条目 + + // 如果解析后白名单为空,也允许所有请求 (避免配置错误导致完全锁死) + if (whitelistEntries.length === 0) { + console.warn('IP 白名单设置非空但解析后为空,暂时允许所有 IP。请检查设置。'); + return next(); + } + + let requestIp: ipaddr.IPv4 | ipaddr.IPv6 | null = null; + try { + requestIp = ipaddr.parse(requestIpString); + } catch (e) { + console.warn(`无法解析请求 IP 地址 "${requestIpString}",已拒绝访问。`); + return res.status(403).json({ message: '禁止访问:无效的来源 IP 格式。' }); + } + + // 检查 IP 是否匹配白名单中的任何条目 + const isAllowed = whitelistEntries.some(entry => { + try { + // 尝试解析为 CIDR 范围 + const range = ipaddr.parseCIDR(entry); + // 使用 match 方法检查 IP 是否在范围内 + // 需要根据 IP 类型调用正确的 match 签名 + if (requestIp!.kind() === 'ipv4' && range[0].kind() === 'ipv4') { + return (requestIp! as ipaddr.IPv4).match(range as [ipaddr.IPv4, number]); + } else if (requestIp!.kind() === 'ipv6' && range[0].kind() === 'ipv6') { + // 注意:IPv6 的 match 可能需要特殊处理,取决于 ipaddr.js 的具体实现和类型定义 + // 这里假设 IPv6 的 match 签名与 IPv4 类似,但可能需要调整 + return (requestIp! as ipaddr.IPv6).match(range as [ipaddr.IPv6, number]); + } + // 如果 IP 类型和范围类型不匹配,则认为不匹配 + return false; + } catch (e1) { + // 如果解析 CIDR 失败,尝试解析为单个 IP 地址 + try { + const allowedIp = ipaddr.parse(entry); + // 比较地址是否相同 + return requestIp!.kind() === allowedIp.kind() && requestIp!.toString() === allowedIp.toString(); + } catch (e2) { + // 如果单个 IP 也解析失败,忽略此条目并记录警告 + console.warn(`无效的 IP 白名单条目: "${entry}"`); + return false; + } + } + }); + + if (isAllowed) { + // IP 在白名单内,允许继续处理请求 + return next(); + } else { + // IP 不在白名单内,拒绝访问 + console.warn(`已拒绝来自 IP ${requestIpString} 的访问 (不在白名单内)。`); + return res.status(403).json({ message: '禁止访问:您的 IP 地址不在允许列表中。' }); + } + + } catch (error: any) { + console.error('IP 白名单中间件执行出错:', error); + // 中间件出错时,为安全起见,默认拒绝访问 + return res.status(500).json({ message: '服务器内部错误 (IP 校验失败)。' }); + } +}; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 19884d0..4aa1b50 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,6 +15,7 @@ import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由 import tagsRouter from './tags/tags.routes'; // 导入标签路由 import settingsRoutes from './settings/settings.routes'; // 导入设置路由 import { initializeWebSocket } from './websocket'; +import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件 // 基础 Express 应用设置 (后续会扩展) const app = express(); @@ -25,6 +26,9 @@ const SQLiteStore = connectSqlite3(session); const dbPath = path.resolve(__dirname, '../../data'); // 数据库目录路径 // --- 中间件 --- +// !! 重要:IP 白名单应尽可能早地应用,通常在其他中间件之前 !! +app.use(ipWhitelistMiddleware as RequestHandler); // 应用 IP 白名单中间件 (使用类型断言) + app.use(express.json()); // 添加此行以解析 JSON 请求体 // 会话中间件配置 diff --git a/packages/backend/src/migrations.ts b/packages/backend/src/migrations.ts index 6a2e7d1..07e58bd 100644 --- a/packages/backend/src/migrations.ts +++ b/packages/backend/src/migrations.ts @@ -1,224 +1,82 @@ import { Database } from 'sqlite3'; import { getDb } from './database'; -const createUsersTableSQL = ` -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - hashed_password TEXT NOT NULL, - two_factor_secret TEXT NULL, -- 2FA 密钥占位符 - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL +const createSettingsTableSQL = ` +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) ); `; -// 更新后的 Schema,支持密码和密钥认证 -const createConnectionsTableSQL = ` -CREATE TABLE IF NOT EXISTS connections ( +const createAuditLogsTableSQL = ` +CREATE TABLE IF NOT EXISTS audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + action_type TEXT NOT NULL, + details TEXT NULL +); +`; + +const createApiKeysTableSQL = ` +CREATE TABLE IF NOT EXISTS api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - host TEXT NOT NULL, - port INTEGER NOT NULL DEFAULT 22, - username TEXT NOT NULL, - auth_method TEXT NOT NULL CHECK(auth_method IN ('password', 'key')), -- 更新 CHECK 约束 - encrypted_password TEXT NULL, - encrypted_private_key TEXT NULL, - encrypted_passphrase TEXT NULL, - proxy_id INTEGER NULL, -- 新增:关联的代理 ID - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - last_connected_at INTEGER NULL, - FOREIGN KEY (proxy_id) REFERENCES proxies(id) ON DELETE SET NULL -- 设置外键约束,删除代理时将关联设为 NULL + hashed_key TEXT UNIQUE NOT NULL, + created_at INTEGER NOT NULL ); `; -// 新增:创建 proxies 表的 SQL (与文档同步) -const createProxiesTableSQL = ` -CREATE TABLE IF NOT EXISTS proxies ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - type TEXT NOT NULL CHECK(type IN ('SOCKS5', 'HTTP')), -- 代理类型,目前支持 SOCKS5 和 HTTP - host TEXT NOT NULL, - port INTEGER NOT NULL, - username TEXT NULL, -- 代理认证用户名 (可选) - auth_method TEXT NOT NULL DEFAULT 'none' CHECK(auth_method IN ('none', 'password', 'key')), -- 添加 auth_method - encrypted_password TEXT NULL, -- 加密存储的代理密码 (可选) - encrypted_private_key TEXT NULL, -- 添加 encrypted_private_key - encrypted_passphrase TEXT NULL, -- 添加 encrypted_passphrase - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL -); -`; - -// 新增:创建 tags 表的 SQL -const createTagsTableSQL = ` -CREATE TABLE IF NOT EXISTS tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, -- 标签名称,唯一 - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL -); -`; - -// 新增:创建 connection_tags 关联表的 SQL -const createConnectionTagsTableSQL = ` -CREATE TABLE IF NOT EXISTS connection_tags ( - connection_id INTEGER NOT NULL, - tag_id INTEGER NOT NULL, - PRIMARY KEY (connection_id, tag_id), - FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE, -- 删除连接时,自动删除关联 - FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE -- 删除标签时,自动删除关联 -); -`; - -// 未来可能需要的其他表 (根据项目文档) -// const createSettingsTableSQL = \`...\`; // 设置表 -// const createAuditLogsTableSQL = \`...\`; // 审计日志表 -// const createApiKeysTableSQL = \`...\`; // API 密钥表 - -// Interface for PRAGMA table_info result rows -interface TableInfoColumn { - cid: number; - name: string; - type: string; - notnull: number; - dflt_value: any; - pk: number; -} - -// Helper function to add a column if it doesn't exist -const addColumnIfNotExists = (db: Database, tableName: string, columnName: string, columnDefinition: string): Promise => { - return new Promise((resolve, reject) => { - // Check if the column exists using PRAGMA table_info - // Explicitly type the 'columns' parameter - db.all(`PRAGMA table_info(${tableName})`, (err, columns: TableInfoColumn[]) => { - if (err) { - console.error(`Error checking table info for ${tableName}:`, err.message); - return reject(err); - } - // Now 'col' inside .some() will have the correct type - const columnExists = columns.some(col => col.name === columnName); - if (!columnExists) { - // Column doesn't exist, add it - const sql = `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition}`; - db.run(sql, (alterErr) => { - if (alterErr) { - console.error(`Error adding column ${columnName} to ${tableName}:`, alterErr.message); - // Don't reject immediately, maybe it's a harmless error (like constraint issue) - // Let subsequent migrations try. If it's critical, the app might fail later. - console.warn(`Potential harmless error adding column ${columnName}. Continuing migration.`); - resolve(); - // return reject(alterErr); - } else { - console.log(`Column ${columnName} added to table ${tableName}.`); - resolve(); - } - }); - } else { - // Column already exists - // console.log(`Column ${columnName} already exists in table ${tableName}.`); - resolve(); - } - }); - }); -}; - - -/** - * 执行数据库迁移 (创建表和添加列) - * @param db - 数据库实例 - * @returns Promise,在所有迁移完成后 resolve - */ export const runMigrations = async (db: Database): Promise => { - // Use async/await for better readability with sequential operations try { + // 创建 settings 表 (如果不存在) await new Promise((resolve, reject) => { - db.run(createUsersTableSQL, (err) => { - if (err) return reject(new Error(`创建 users 表时出错: ${err.message}`)); - console.log('Users 表已检查/创建。'); + db.run(createSettingsTableSQL, (err: Error | null) => { + if (err) return reject(new Error(`创建 settings 表时出错: ${err.message}`)); + console.log('Settings 表已检查/创建。'); + resolve(); + }); + }); + + // 插入默认的 IP 白名单设置 + await new Promise((resolve, reject) => { + db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('ipWhitelistEnabled', 'false')", (err: Error | null) => { + if (err) return reject(new Error(`插入默认 ipWhitelistEnabled 设置时出错: ${err.message}`)); + console.log('默认 ipWhitelistEnabled 设置已插入。'); resolve(); }); }); await new Promise((resolve, reject) => { - db.run(createConnectionsTableSQL, (err) => { - // Ignore "duplicate column name" error if table already exists partially - if (err && !err.message.includes('duplicate column name')) { - return reject(new Error(`创建 connections 表时出错: ${err.message}`)); - } - if (err && err.message.includes('duplicate column name')) { - console.warn('创建 connections 表时遇到 "duplicate column name" 错误,可能表已部分存在,将尝试 ALTER TABLE。'); - } - console.log('Connections 表已检查/尝试创建。'); + db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('ipWhitelist', '')", (err: Error | null) => { + if (err) return reject(new Error(`插入默认 ipWhitelist 设置时出错: ${err.message}`)); + console.log('默认 ipWhitelist 设置已插入。'); resolve(); }); }); - // Add columns to connections table if they don't exist - // Add auth_method first in case it's missing from very old schema - await addColumnIfNotExists(db, 'connections', 'auth_method', "TEXT NOT NULL DEFAULT 'password'"); // Add default for existing rows - await addColumnIfNotExists(db, 'connections', 'encrypted_private_key', 'TEXT NULL'); - await addColumnIfNotExists(db, 'connections', 'encrypted_passphrase', 'TEXT NULL'); - // 新增:添加 proxy_id 列到 connections 表 (如果不存在) - // 注意:直接添加带 FOREIGN KEY 的列在旧版 SQLite 中可能有限制,但现代版本通常支持。 - // 如果遇到问题,可能需要更复杂的迁移步骤(创建新表,复制数据,重命名)。 - // 这里我们先尝试直接添加。ON DELETE SET NULL 意味着如果代理被删除,关联的连接不会被删除,只是 proxy_id 变为空。 - await addColumnIfNotExists(db, 'connections', 'proxy_id', 'INTEGER NULL REFERENCES proxies(id) ON DELETE SET NULL'); - - // 创建 proxies 表 (如果不存在) + // 创建 audit_logs 表 (如果不存在) await new Promise((resolve, reject) => { - db.run(createProxiesTableSQL, (err) => { - if (err) return reject(new Error(`创建 proxies 表时出错: ${err.message}`)); - console.log('Proxies 表已检查/创建。'); + db.run(createAuditLogsTableSQL, (err: Error | null) => { + if (err) return reject(new Error(`创建 audit_logs 表时出错: ${err.message}`)); + console.log('Audit_Logs 表已检查/创建。'); resolve(); }); }); - // Add columns to proxies table if they don't exist (to match documentation) - await addColumnIfNotExists(db, 'proxies', 'auth_method', "TEXT NOT NULL DEFAULT 'none'"); - await addColumnIfNotExists(db, 'proxies', 'encrypted_private_key', 'TEXT NULL'); - await addColumnIfNotExists(db, 'proxies', 'encrypted_passphrase', 'TEXT NULL'); - - - // 新增:创建 tags 表 (如果不存在) + // 创建 api_keys 表 (如果不存在) await new Promise((resolve, reject) => { - db.run(createTagsTableSQL, (err) => { - if (err) return reject(new Error(`创建 tags 表时出错: ${err.message}`)); - console.log('Tags 表已检查/创建。'); + db.run(createApiKeysTableSQL, (err: Error | null) => { + if (err) return reject(new Error(`创建 api_keys 表时出错: ${err.message}`)); + console.log('Api_Keys 表已检查/创建。'); resolve(); }); }); - // 新增:创建 connection_tags 表 (如果不存在) - await new Promise((resolve, reject) => { - db.run(createConnectionTagsTableSQL, (err) => { - if (err) return reject(new Error(`创建 connection_tags 表时出错: ${err.message}`)); - console.log('Connection_Tags 表已检查/创建。'); - resolve(); - }); - }); - - // Add other tables or columns here in the future - - console.log('数据库迁移检查完成。'); - + console.log('所有数据库迁移已完成。'); } catch (error) { - console.error('数据库迁移过程中发生错误:', error); - throw error; // Re-throw the error to be caught by the caller + console.error('数据库迁移过程中出错:', error); + throw error; } }; - -// 允许通过命令行直接运行此文件来执行迁移 (例如: node dist/migrations.js) -if (require.main === module) { - const db = getDb(); - runMigrations(db) - .then(() => { - console.log('数据库迁移执行成功。'); - // 如果是独立运行,可以选择关闭数据库连接,但在应用启动流程中通常不需要 - // db.close(); - }) - .catch((err) => { - console.error('数据库迁移执行失败:', err); - process.exit(1); - }); -} diff --git a/packages/backend/src/repositories/settings.repository.ts b/packages/backend/src/repositories/settings.repository.ts index a4ddf98..6d9f528 100644 --- a/packages/backend/src/repositories/settings.repository.ts +++ b/packages/backend/src/repositories/settings.repository.ts @@ -36,9 +36,14 @@ export const settingsRepository = { async setSetting(key: string, value: string): Promise { return new Promise((resolve, reject) => { + const now = Math.floor(Date.now() / 1000); // 获取当前 Unix 时间戳 db.run( - 'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', - [key, value], + `INSERT INTO settings (key, value, created_at, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at`, + [key, value, now, now], function (err: any) { // 添加 err 类型 if (err) { console.error(`设置设置项 ${key} 时出错:`, err); // 更新日志为中文 diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index 0717855..c09f4d0 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -47,4 +47,29 @@ export const settingsService = { async deleteSetting(key: string): Promise { await settingsRepository.deleteSetting(key); }, + + /** + * 获取 IP 白名单设置 + * @returns 返回包含启用状态和白名单列表的对象 + */ + async getIpWhitelistSettings(): Promise<{ enabled: boolean; whitelist: string }> { + const enabledStr = await settingsRepository.getSetting('ipWhitelistEnabled'); + const whitelist = await settingsRepository.getSetting('ipWhitelist'); + return { + enabled: enabledStr === 'true', // 将字符串 'true' 转换为布尔值 + whitelist: whitelist ?? '', // 如果为 null 则返回空字符串 + }; + }, + + /** + * 更新 IP 白名单设置 + * @param enabled 是否启用 IP 白名单 + * @param whitelist 允许的 IP 地址/CIDR 列表 (字符串形式) + */ + async updateIpWhitelistSettings(enabled: boolean, whitelist: string): Promise { + await Promise.all([ + settingsRepository.setSetting('ipWhitelistEnabled', String(enabled)), // 将布尔值转换为字符串 + settingsRepository.setSetting('ipWhitelist', whitelist), + ]); + }, }; diff --git a/packages/frontend/src/locales/en.json b/packages/frontend/src/locales/en.json index 2f26ecd..e0a3644 100644 --- a/packages/frontend/src/locales/en.json +++ b/packages/frontend/src/locales/en.json @@ -324,6 +324,20 @@ "passwordRequiredForDisable": "Current password is required to disable.", "disableFailed": "Failed to disable two-factor authentication." } + }, + "ipWhitelist": { + "title": "IP Whitelist", + "description": "Configure allowed IP addresses or ranges to access this application. Leave empty to allow all IPs.", + "label": "Allowed IP Addresses/Ranges (one per line or comma-separated):", + "hint": "Supports IPv4, IPv6, and CIDR (e.g., 192.168.1.100, 10.0.0.0/8, 2001:db8::/32).", + "saveButton": "Save Whitelist", + "success": { + "saved": "IP whitelist saved successfully." + }, + "error": { + "fetchFailed": "Failed to fetch IP whitelist settings.", + "saveFailed": "Failed to save IP whitelist." + } } }, "common": { diff --git a/packages/frontend/src/locales/zh.json b/packages/frontend/src/locales/zh.json index 0858310..003f370 100644 --- a/packages/frontend/src/locales/zh.json +++ b/packages/frontend/src/locales/zh.json @@ -327,6 +327,20 @@ "passwordRequiredForDisable": "需要输入当前密码才能禁用。", "disableFailed": "禁用两步验证失败。" } + }, + "ipWhitelist": { + "title": "IP 白名单", + "description": "配置允许访问此应用的 IP 地址或范围。留空则允许所有 IP。", + "label": "允许的 IP 地址/范围 (每行一个或用逗号分隔):", + "hint": "支持 IPv4, IPv6 和 CIDR (例如 192.168.1.100, 10.0.0.0/8, 2001:db8::/32)。", + "saveButton": "保存白名单", + "success": { + "saved": "IP 白名单已成功保存。" + }, + "error": { + "fetchFailed": "获取 IP 白名单设置失败。", + "saveFailed": "保存 IP 白名单失败。" + } } }, "common": { diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index 5cbc7ca..b28e8dc 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -69,6 +69,24 @@

{{ twoFactorMessage }}

+
+ +
+

{{ $t('settings.ipWhitelist.title') }}

+

{{ $t('settings.ipWhitelist.description') }}

+
+
+ + + {{ $t('settings.ipWhitelist.hint') }} +
+ +

{{ ipWhitelistMessage }}

+
+
+ + + @@ -98,6 +116,12 @@ const setupData = ref<{ secret: string; qrCodeUrl: string } | null>(null); // const verificationCode = ref(''); // 用户输入的验证码 const disablePassword = ref(''); // 禁用时需要输入的密码 +// --- IP 白名单状态 --- +const ipWhitelistInput = ref(''); // 用于编辑的文本区域内容 +const ipWhitelistLoading = ref(false); +const ipWhitelistMessage = ref(''); +const ipWhitelistSuccess = ref(false); + // 计算属性判断当前是否处于 2FA 设置流程中 const isSettingUp2FA = computed(() => setupData.value !== null); @@ -109,8 +133,27 @@ const checkTwoFactorStatus = async () => { twoFactorEnabled.value = authStore.user?.isTwoFactorEnabled ?? false; }; +// 获取当前的 IP 白名单设置 +const fetchIpWhitelist = async () => { + ipWhitelistLoading.value = true; + ipWhitelistMessage.value = ''; + try { + // 使用 settings API 获取所有设置 + const response = await axios.get>('/api/v1/settings'); + ipWhitelistInput.value = response.data['ipWhitelist'] || ''; // 从设置中获取,默认为空字符串 + } catch (error: any) { + console.error('获取 IP 白名单设置失败:', error); + ipWhitelistMessage.value = t('settings.ipWhitelist.error.fetchFailed'); + ipWhitelistSuccess.value = false; + } finally { + ipWhitelistLoading.value = false; + } +}; + + onMounted(async () => { // 使 onMounted 异步 await checkTwoFactorStatus(); // 等待状态检查完成 + await fetchIpWhitelist(); // 获取 IP 白名单设置 }); @@ -222,6 +265,28 @@ const cancelSetup = () => { twoFactorMessage.value = ''; }; +// --- IP 白名单相关方法 --- +const handleUpdateIpWhitelist = async () => { + ipWhitelistLoading.value = true; + ipWhitelistMessage.value = ''; + ipWhitelistSuccess.value = false; + + try { + // 调用 settings API 更新设置 + await axios.put('/api/v1/settings', { + ipWhitelist: ipWhitelistInput.value.trim() // 发送修剪后的值 + }); + ipWhitelistMessage.value = t('settings.ipWhitelist.success.saved'); + ipWhitelistSuccess.value = true; + } catch (error: any) { + console.error('更新 IP 白名单失败:', error); + ipWhitelistMessage.value = error.response?.data?.message || t('settings.ipWhitelist.error.saveFailed'); + ipWhitelistSuccess.value = false; + } finally { + ipWhitelistLoading.value = false; + } +}; +