This commit is contained in:
Baobhan Sith
2025-04-29 16:03:41 +08:00
parent cfd98e53c7
commit 731015e893
6 changed files with 129 additions and 19 deletions
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { ipBlacklistService } from '../services/ip-blacklist.service'; import { ipBlacklistService } from '../services/ip-blacklist.service';
import { settingsService } from '../services/settings.service'; // <-- Import settingsService
/** /**
* IP 黑名单检查中间件 * IP 黑名单检查中间件
@@ -17,6 +18,13 @@ export const ipBlacklistCheckMiddleware = async (req: Request, res: Response, ne
} }
try { try {
// 首先检查 IP 黑名单功能是否启用
const isEnabled = await settingsService.isIpBlacklistEnabled();
if (!isEnabled) {
// console.log('[IP Blacklist Check] 功能已禁用,跳过检查。');
return next(); // 功能禁用,直接放行
}
const isBlocked = await ipBlacklistService.isBlocked(clientIp); const isBlocked = await ipBlacklistService.isBlocked(clientIp);
if (isBlocked) { if (isBlocked) {
console.warn(`[IP Blacklist Check] 已阻止来自被封禁 IP ${clientIp} 的访问。`); console.warn(`[IP Blacklist Check] 已阻止来自被封禁 IP ${clientIp} 的访问。`);
@@ -51,10 +51,16 @@ export class IpBlacklistService {
* @param ip IP 地址 * @param ip IP 地址
* @returns 如果被封禁则返回 true,否则返回 false * @returns 如果被封禁则返回 true,否则返回 false
*/ */
async isBlocked(ip: string): Promise<boolean> { async isBlocked(ip: string): Promise<boolean> {
try { // 首先检查功能是否启用
const entry = await this.getEntry(ip); if (!(await settingsService.isIpBlacklistEnabled())) {
if (!entry) { // console.log('[IP Blacklist] 功能已禁用,跳过 isBlocked 检查。');
return false; // 如果禁用,则认为 IP 未被阻止
}
try {
const entry = await this.getEntry(ip);
if (!entry) {
return false; // 不在黑名单中 return false; // 不在黑名单中
} }
// 检查封禁时间是否已过 // 检查封禁时间是否已过
@@ -75,6 +81,12 @@ export class IpBlacklistService {
* @param ip IP 地址 * @param ip IP 地址
*/ */
async recordFailedAttempt(ip: string): Promise<void> { async recordFailedAttempt(ip: string): Promise<void> {
// 首先检查功能是否启用
if (!(await settingsService.isIpBlacklistEnabled())) {
// console.log('[IP Blacklist] 功能已禁用,跳过 recordFailedAttempt。');
return; // 如果禁用,则不记录失败尝试
}
if (LOCAL_IPS.includes(ip)) { if (LOCAL_IPS.includes(ip)) {
console.log(`[IP Blacklist] 检测到本地 IP ${ip} 登录失败,跳过黑名单处理。`); console.log(`[IP Blacklist] 检测到本地 IP ${ip} 登录失败,跳过黑名单处理。`);
return; return;
@@ -30,6 +30,7 @@ const LAYOUT_TREE_KEY = 'layoutTree'; // 布局树设置键
const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制设置键 const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制设置键
const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键 const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键
const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔 const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔
const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键
export const settingsService = { export const settingsService = {
/** /**
@@ -108,6 +109,24 @@ export const settingsService = {
]); ]);
}, },
/**
* 检查 IP 黑名单功能是否已启用
* @returns 返回是否启用 (boolean),如果未设置则默认为 true
*/
async isIpBlacklistEnabled(): Promise<boolean> {
console.log(`[Service] Attempting to get setting for key: ${IP_BLACKLIST_ENABLED_KEY}`);
try {
const enabledStr = await settingsRepository.getSetting(IP_BLACKLIST_ENABLED_KEY);
console.log(`[Service] Raw value from repository for ${IP_BLACKLIST_ENABLED_KEY}:`, enabledStr);
// 如果设置存在且值为 'false',则返回 false,否则都返回 true (包括未设置的情况)
return enabledStr !== 'false';
} catch (error) {
console.error(`[Service] Error getting IP blacklist enabled setting (key: ${IP_BLACKLIST_ENABLED_KEY}):`, error);
// 出错时返回默认值 true (安全起见,默认启用)
return true;
}
}, // *** 确保这里有逗号 ***
/** /**
* 获取焦点切换顺序 * 获取焦点切换顺序
* @returns 返回存储的完整焦点切换配置对象,如果未设置或无效则返回默认空配置 * @returns 返回存储的完整焦点切换配置对象,如果未设置或无效则返回默认空配置
@@ -46,7 +46,8 @@ export const settingsController = {
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++ 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone', // NEW: 添加时区键 'timezone', // NEW: 添加时区键
'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键 'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键
'rdpModalHeight' // NEW: 添加 RDP 模态框高度键 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
'ipBlacklistEnabled' // <-- 添加 IP 黑名单启用键
]; ];
const filteredSettings: Record<string, string> = {}; const filteredSettings: Record<string, string> = {};
for (const key in settingsToUpdate) { for (const key in settingsToUpdate) {
+16 -2
View File
@@ -47,6 +47,7 @@ interface SettingsState {
timezone?: string; // NEW: 时区设置 (e.g., 'Asia/Shanghai', 'UTC') timezone?: string; // NEW: 时区设置 (e.g., 'Asia/Shanghai', 'UTC')
rdpModalWidth?: string; // NEW: RDP 模态框宽度 rdpModalWidth?: string; // NEW: RDP 模态框宽度
rdpModalHeight?: string; // NEW: RDP 模态框高度 rdpModalHeight?: string; // NEW: RDP 模态框高度
ipBlacklistEnabled?: string; // <-- NEW: IP 黑名单启用状态 'true' or 'false'
// Add other general settings keys here as needed // Add other general settings keys here as needed
[key: string]: string | undefined; // Allow other string settings [key: string]: string | undefined; // Allow other string settings
} }
@@ -100,6 +101,11 @@ export const useSettingsStore = defineStore('settings', () => {
} }
// NEW: IP Blacklist enabled default
if (settings.value.ipBlacklistEnabled === undefined) {
settings.value.ipBlacklistEnabled = 'true'; // 默认启用 IP 黑名单
}
if (settings.value.autoCopyOnSelect === undefined) { if (settings.value.autoCopyOnSelect === undefined) {
settings.value.autoCopyOnSelect = 'false'; // 默认禁用选中即复制 settings.value.autoCopyOnSelect = 'false'; // 默认禁用选中即复制
} }
@@ -299,7 +305,8 @@ export const useSettingsStore = defineStore('settings', () => {
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++ 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone', // NEW: 添加时区键 'timezone', // NEW: 添加时区键
'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键 'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键
'rdpModalHeight' // NEW: 添加 RDP 模态框高度键 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
'ipBlacklistEnabled' // <-- NEW: 添加 IP 黑名单启用键
]; ];
if (!allowedKeys.includes(key)) { if (!allowedKeys.includes(key)) {
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
@@ -358,7 +365,8 @@ export const useSettingsStore = defineStore('settings', () => {
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++ 'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone', // NEW: 添加时区键 'timezone', // NEW: 添加时区键
'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键 'rdpModalWidth', // NEW: 添加 RDP 模态框宽度键
'rdpModalHeight' // NEW: 添加 RDP 模态框高度键 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
'ipBlacklistEnabled' // <-- NEW: 添加 IP 黑名单启用键
]; ];
const filteredUpdates: Partial<SettingsState> = {}; const filteredUpdates: Partial<SettingsState> = {};
let languageUpdate: string | undefined = undefined; // Use string type let languageUpdate: string | undefined = undefined; // Use string type
@@ -530,6 +538,11 @@ export const useSettingsStore = defineStore('settings', () => {
// Getter for IP Whitelist enabled status // Getter for IP Whitelist enabled status
const ipWhitelistEnabled = computed(() => settings.value.ipWhitelistEnabled === 'true'); const ipWhitelistEnabled = computed(() => settings.value.ipWhitelistEnabled === 'true');
// <-- NEW: Getter for IP Blacklist enabled status -->
const ipBlacklistEnabledBoolean = computed(() => {
// Default to true if the setting is missing or not 'false'
return settings.value.ipBlacklistEnabled !== 'false';
});
// Getter for auto copy on select setting, returning boolean // Getter for auto copy on select setting, returning boolean
const autoCopyOnSelectBoolean = computed(() => { const autoCopyOnSelectBoolean = computed(() => {
@@ -600,6 +613,7 @@ export const useSettingsStore = defineStore('settings', () => {
showPopupFileEditorBoolean, showPopupFileEditorBoolean,
shareFileEditorTabsBoolean, shareFileEditorTabsBoolean,
ipWhitelistEnabled, // 暴露 IP 白名单启用状态 ipWhitelistEnabled, // 暴露 IP 白名单启用状态
ipBlacklistEnabledBoolean, // <-- NEW: 暴露 IP 黑名单启用状态 getter
autoCopyOnSelectBoolean, autoCopyOnSelectBoolean,
dockerDefaultExpandBoolean, // +++ 暴露 Docker 默认展开 getter +++ dockerDefaultExpandBoolean, // +++ 暴露 Docker 默认展开 getter +++
statusMonitorIntervalSecondsNumber, // +++ 暴露状态监控间隔 getter +++ statusMonitorIntervalSecondsNumber, // +++ 暴露状态监控间隔 getter +++
+68 -12
View File
@@ -199,13 +199,38 @@
<!-- IP Blacklist Section: Only show if settings data is loaded (as config depends on it) --> <!-- IP Blacklist Section: Only show if settings data is loaded (as config depends on it) -->
<div v-if="settings" class="bg-background border border-border rounded-lg shadow-sm overflow-hidden"> <div v-if="settings" class="bg-background border border-border rounded-lg shadow-sm overflow-hidden">
<h2 class="text-lg font-semibold text-foreground px-6 py-4 border-b border-border bg-header/50">{{ $t('settings.ipBlacklist.title') }}</h2> <div class="flex items-center justify-between px-6 py-4 border-b border-border bg-header/50">
<h2 class="text-lg font-semibold text-foreground">{{ $t('settings.ipBlacklist.title') }}</h2>
<!-- IP Blacklist Enable/Disable Switch -->
<button
type="button"
@click="handleUpdateIpBlacklistEnabled"
:class="[
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary',
ipBlacklistEnabled ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
]"
role="switch"
:aria-checked="ipBlacklistEnabled"
>
<span
aria-hidden="true"
:class="[
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
ipBlacklistEnabled ? 'translate-x-5' : 'translate-x-0'
]"
></span>
</button>
</div>
<div class="p-6 space-y-6"> <div class="p-6 space-y-6">
<p class="text-sm text-text-secondary">{{ $t('settings.ipBlacklist.description') }}</p> <!-- Description moved below -->
<!-- Blacklist config form -->
<form @submit.prevent="handleUpdateBlacklistSettings" class="flex flex-wrap items-end gap-4 pt-4 border-t border-border/50"> <!-- Existing Blacklist Content (Conditional Rendering) -->
<div class="flex-grow min-w-[150px]"> <div v-if="ipBlacklistEnabled" class="space-y-6 pt-4">
<label for="maxLoginAttempts" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.ipBlacklist.maxAttemptsLabel') }}</label> <p class="text-sm text-text-secondary">{{ $t('settings.ipBlacklist.description') }}</p>
<!-- Blacklist config form -->
<form @submit.prevent="handleUpdateBlacklistSettings" class="flex flex-wrap items-end gap-4">
<div class="flex-grow min-w-[150px]">
<label for="maxLoginAttempts" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.ipBlacklist.maxAttemptsLabel') }}</label>
<input type="number" id="maxLoginAttempts" v-model="blacklistSettingsForm.maxLoginAttempts" min="1" required <input type="number" id="maxLoginAttempts" v-model="blacklistSettingsForm.maxLoginAttempts" min="1" 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"> 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>
@@ -220,11 +245,11 @@
{{ $t('settings.ipBlacklist.saveConfigButton') }} {{ $t('settings.ipBlacklist.saveConfigButton') }}
</button> </button>
</div> </div>
<p v-if="blacklistSettingsMessage" :class="['w-full mt-2 text-sm', blacklistSettingsSuccess ? 'text-success' : 'text-error']">{{ blacklistSettingsMessage }}</p> <p v-if="blacklistSettingsMessage" :class="['w-full mt-2 text-sm', blacklistSettingsSuccess ? 'text-success' : 'text-error']">{{ blacklistSettingsMessage }}</p>
</form> </form>
<hr class="border-border/50"> <hr class="border-border/50">
<!-- Blacklist table --> <!-- Blacklist table -->
<h3 class="text-base font-semibold text-foreground">{{ $t('settings.ipBlacklist.currentBannedTitle') }}</h3> <h3 class="text-base font-semibold text-foreground">{{ $t('settings.ipBlacklist.currentBannedTitle') }}</h3>
<!-- Error state --> <!-- Error state -->
<div v-if="ipBlacklist.error" class="p-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ ipBlacklist.error }}</div> <div v-if="ipBlacklist.error" class="p-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ ipBlacklist.error }}</div>
<!-- Loading state (Only show if loading AND no entries are displayed yet) --> <!-- Loading state (Only show if loading AND no entries are displayed yet) -->
@@ -263,7 +288,12 @@
</table> </table>
</div> </div>
<!-- Delete Error (Show regardless of loading state if present) --> <!-- Delete Error (Show regardless of loading state if present) -->
<p v-if="blacklistDeleteError" class="mt-3 text-sm text-error">{{ blacklistDeleteError }}</p> <p v-if="blacklistDeleteError" class="mt-3 text-sm text-error">{{ blacklistDeleteError }}</p>
</div> <!-- End v-if="ipBlacklistEnabled" -->
<!-- Message when disabled -->
<div v-else class="p-4 text-center text-text-secondary italic border border-dashed border-border/50 rounded-md">
{{ $t('settings.ipBlacklist.disabledMessage', 'IP 黑名单功能当前已禁用。') }}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -547,6 +577,7 @@ const {
workspaceSidebarPersistentBoolean, workspaceSidebarPersistentBoolean,
captchaSettings, // <-- Import CAPTCHA settings state captchaSettings, // <-- Import CAPTCHA settings state
commandInputSyncTarget, // NEW: Import command input sync target getter commandInputSyncTarget, // NEW: Import command input sync target getter
ipBlacklistEnabledBoolean, // <-- Import IP Blacklist enabled getter
} = storeToRefs(settingsStore); } = storeToRefs(settingsStore);
// Removed Passkey state import from authStore // Removed Passkey state import from authStore
@@ -570,6 +601,7 @@ const blacklistSettingsForm = reactive({ // Renamed to avoid conflict with store
const popupEditorEnabled = ref(true); // 本地状态,用于 v-model const popupEditorEnabled = ref(true); // 本地状态,用于 v-model
const workspaceSidebarPersistentEnabled = ref(false); // 新增:侧边栏固定设置的本地状态 const workspaceSidebarPersistentEnabled = ref(false); // 新增:侧边栏固定设置的本地状态
const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none'); // NEW: Local state for command input sync target const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none'); // NEW: Local state for command input sync target
const ipBlacklistEnabled = ref(true); // <-- Local state for IP Blacklist switch
// --- Local UI feedback state --- // --- Local UI feedback state ---
const ipWhitelistLoading = ref(false); const ipWhitelistLoading = ref(false);
@@ -581,6 +613,9 @@ const languageSuccess = ref(false);
const blacklistSettingsLoading = ref(false); const blacklistSettingsLoading = ref(false);
const blacklistSettingsMessage = ref(''); const blacklistSettingsMessage = ref('');
const blacklistSettingsSuccess = ref(false); const blacklistSettingsSuccess = ref(false);
// Removed ipBlacklistEnabledLoading, ipBlacklistEnabledMessage, ipBlacklistEnabledSuccess refs
const popupEditorLoading = ref(false); const popupEditorLoading = ref(false);
const popupEditorMessage = ref(''); const popupEditorMessage = ref('');
const popupEditorSuccess = ref(false); const popupEditorSuccess = ref(false);
@@ -658,6 +693,7 @@ watch(settings, (newSettings, oldSettings) => {
workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; // 新增:同步侧边栏固定设置 workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; // 新增:同步侧边栏固定设置
commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target
selectedTimezone.value = newSettings.timezone || 'UTC'; // 同步时区设置 selectedTimezone.value = newSettings.timezone || 'UTC'; // 同步时区设置
ipBlacklistEnabled.value = ipBlacklistEnabledBoolean.value; // <-- Sync IP Blacklist enabled state
}, { deep: true, immediate: true }); // immediate: true to run on initial load }, { deep: true, immediate: true }); // immediate: true to run on initial load
@@ -1069,6 +1105,26 @@ const handleUpdateBlacklistSettings = async () => {
} }
}; };
// --- IP Blacklist Enable/Disable Method (Button Style) ---
const handleUpdateIpBlacklistEnabled = async () => {
// Toggle local state immediately for instant UI feedback
const originalValue = ipBlacklistEnabled.value;
ipBlacklistEnabled.value = !ipBlacklistEnabled.value;
try {
const valueToSave = ipBlacklistEnabled.value ? 'true' : 'false';
await settingsStore.updateSetting('ipBlacklistEnabled', valueToSave);
// Save successful, no message needed for toggle switch
console.log('IP Blacklist enabled status saved:', valueToSave);
} catch (error: any) {
console.error('更新 IP 黑名单启用状态失败:', error);
// Optionally show error notification to user here
// Revert button state on failure
ipBlacklistEnabled.value = originalValue; // Revert to original value
}
// No loading/message state management needed
};
// --- CAPTCHA Settings Method --- // --- CAPTCHA Settings Method ---
const handleUpdateCaptchaSettings = async () => { const handleUpdateCaptchaSettings = async () => {
captchaLoading.value = true; captchaLoading.value = true;