feat: 添加通知功能自定义telegram域名功能

This commit is contained in:
Baobhan Sith
2025-05-11 13:02:09 +08:00
parent 598df938bf
commit d7bee11383
34 changed files with 121 additions and 71 deletions
@@ -121,7 +121,7 @@ export const uploadTerminalBackgroundController = async (req: Request, res: Resp
};
/**
* 新增:获取背景图片文件
* 获取背景图片文件
*/
export const getBackgroundFileController = async (req: Request, res: Response): Promise<void> => {
const filename = req.params.filename;
@@ -27,7 +27,7 @@ router.post(
appearanceController.uploadTerminalBackgroundController
);
// 新增:GET /api/v1/appearance/background/file/:filename - 获取背景图片文件
// GET /api/v1/appearance/background/file/:filename - 获取背景图片文件
router.get('/background/file/:filename', appearanceController.getBackgroundFileController);
// DELETE /api/v1/appearance/background/page - 删除页面背景图片
+1 -1
View File
@@ -19,7 +19,7 @@ import {
// 新的 Passkey 管理处理器
listUserPasskeysHandler,
deleteUserPasskeyHandler,
updateUserPasskeyNameHandler, // 新增:更新 Passkey 名称的处理器
updateUserPasskeyNameHandler, // 更新 Passkey 名称的处理器
checkHasPasskeys
} from './auth.controller';
import { isAuthenticated } from './auth.middleware';
@@ -18,7 +18,7 @@ interface DbAppearanceSettingsRow {
const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): AppearanceSettings => {
const settings: Partial<AppearanceSettings> = {};
let latestUpdatedAt = 0;
let terminalBackgroundEnabledFound = false; // 新增:标记是否在数据库中找到该设置
let terminalBackgroundEnabledFound = false; // 标记是否在数据库中找到该设置
for (const row of rows) {
// 更新 latestUpdatedAt
@@ -85,7 +85,7 @@ const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
editorFontSize: 14,
terminalBackgroundImage: undefined,
pageBackgroundImage: undefined,
terminalBackgroundEnabled: true, // 新增:默认启用
terminalBackgroundEnabled: true, // 默认启用
updatedAt: Date.now(), // 提供默认时间戳
};
};
@@ -249,7 +249,7 @@ const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDt
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null'
} else if (typeof value === 'object') {
dbValue = JSON.stringify(value);
} else if (typeof value === 'boolean') { // 新增:处理布尔值
} else if (typeof value === 'boolean') { // 处理布尔值
dbValue = value ? 'true' : 'false';
} else {
dbValue = String(value);
@@ -356,7 +356,17 @@ export class NotificationService {
);
console.log(`[通知测试 - Telegram] 渲染的消息文本:`, messageText);
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
let baseApiUrl = "https://api.telegram.org";
if (config.customDomain) {
try {
const url = new URL(config.customDomain);
baseApiUrl = `${url.protocol}//${url.host}`;
console.log(`[通知测试 - Telegram] 使用自定义域名: ${baseApiUrl}`);
} catch (e) {
console.warn(`[通知测试 - Telegram] 无效的自定义域名 URL: ${config.customDomain}。将回退到默认 Telegram API。`);
}
}
const telegramApiUrl = `${baseApiUrl}/bot${config.botToken}/sendMessage`;
try {
console.log(
@@ -802,7 +812,18 @@ export class NotificationService {
}
console.log(`[_sendTelegram] Final message text to send:`, messageText);
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
let baseApiUrlSend = "https://api.telegram.org";
if (config.customDomain) {
try {
const url = new URL(config.customDomain);
baseApiUrlSend = `${url.protocol}//${url.host}`;
console.log(`[_sendTelegram] 使用自定义域名: ${baseApiUrlSend} (事件: ${payload.event})`);
} catch (e) {
console.warn(`[_sendTelegram] 无效的自定义域名 URL: ${config.customDomain} (事件: ${payload.event})。将回退到默认 Telegram API。`);
}
}
const telegramApiUrl = `${baseApiUrlSend}/bot${config.botToken}/sendMessage`;
try {
console.log(
`[通知] 发送 Telegram 消息到聊天 ID ${config.chatId} (事件: ${payload.event})`
@@ -6,7 +6,7 @@ import { TelegramConfig } from "../../types/notification.types";
class TelegramSenderService implements INotificationSender {
async send(notification: ProcessedNotification): Promise<void> {
const config = notification.config as TelegramConfig;
const { botToken, chatId } = config;
const { botToken, chatId, customDomain } = config; // Destructure customDomain
const messageBody = notification.body;
if (!botToken || !chatId) {
@@ -18,7 +18,19 @@ class TelegramSenderService implements INotificationSender {
);
}
const apiUrl = `https://api.telegram.org/bot${botToken}/sendMessage`;
let baseApiUrl = "https://api.telegram.org";
if (customDomain) {
try {
const url = new URL(customDomain); // Validate and parse the custom domain
baseApiUrl = `${url.protocol}//${url.host}`; // Use protocol and host from customDomain
console.log(`[TelegramSender] Using custom domain: ${baseApiUrl}`);
} catch (e) {
console.warn(`[TelegramSender] Invalid customDomain URL: ${customDomain}. Falling back to default Telegram API.`);
// Optionally, you could throw an error here or decide to proceed with the default
}
}
const apiUrl = `${baseApiUrl}/bot${botToken}/sendMessage`;
try {
console.log(
@@ -753,7 +753,7 @@ export class SftpService {
}
try {
// --- 新增:移动前检查目标是否存在 ---
// --- 移动前检查目标是否存在 ---
let targetExists = false;
try {
await this.getStats(sftp, newPath);
@@ -1011,7 +1011,7 @@ export class SftpService {
console.log(`[SFTP Upload ${uploadId}] Starting upload for ${remotePath} (${totalSize} bytes) in session ${sessionId}`);
try {
// --- 新增:在创建流之前确保目录存在 ---
// --- 在创建流之前确保目录存在 ---
if (relativePath) {
const targetDirectory = pathModule.dirname(remotePath).replace(/\\/g, '/');
console.log(`[SFTP Upload ${uploadId}] Ensuring directory exists: ${targetDirectory}`);
@@ -1029,7 +1029,7 @@ export class SftpService {
}
// --- 结束新增 ---
// --- 新增:预检查文件是否可写 ---
// --- 预检查文件是否可写 ---
console.log(`[SFTP Upload ${uploadId}] Pre-checking writability for: ${remotePath}`);
try {
// 确保 state.sftp 存在
@@ -16,7 +16,7 @@ export interface AppearanceSettings {
terminalBackgroundImage?: string; // 终端背景图片 URL 或路径
pageBackgroundImage?: string; // 页面背景图片 URL 或路径
editorFontSize?: number; // 编辑器字体大小 (px)
terminalBackgroundEnabled?: boolean; // 新增:终端背景是否启用
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
updatedAt?: number;
}
@@ -40,6 +40,7 @@ export interface TelegramConfig {
botToken: string; // Consider storing this securely, maybe encrypted or via env vars
chatId: string; // Target chat ID
messageTemplate?: string; // Optional message template
customDomain?: string; // 允许用户自定义 Telegram API 域名
}
export type NotificationChannelConfig = WebhookConfig | EmailConfig | TelegramConfig;
@@ -16,7 +16,7 @@ export function handleRdpProxyConnection(
const rdpWidthStr = (request as any).rdpWidth; // Get as string first
const rdpHeightStr = (request as any).rdpHeight; // Get as string first
// --- 新增:参数验证和 DPI 计算 ---
// --- 参数验证和 DPI 计算 ---
if (!rdpToken || !rdpWidthStr || !rdpHeightStr) { // Check string presence
console.error(`WebSocket: RDP Proxy connection for ${ws.username} missing required parameters (token, width, height).`);
ws.send(JSON.stringify({ type: 'rdp:error', payload: 'Missing RDP connection parameters (token, width, height).' }));
+4 -4
View File
@@ -20,10 +20,10 @@ export interface ClientState { // 导出以便 Service 可以导入
statusIntervalId?: NodeJS.Timeout; // 添加状态轮询 ID (由 StatusMonitorService 管理)
dockerStatusIntervalId?: NodeJS.Timeout; // NEW: Docker 状态轮询 ID
ipAddress?: string; // 添加 IP 地址字段
isShellReady?: boolean; // 新增:标记 Shell 是否已准备好处理输入和调整大小
isSuspendedByService?: boolean; // 新增:标记此会话是否已被 SshSuspendService 接管
isMarkedForSuspend?: boolean; // 新增:标记此会话是否已被用户请求挂起(等待断开连接)
suspendLogPath?: string; // 新增:如果标记挂起,则存储日志路径 (基于原始 sessionId)
isShellReady?: boolean; // 标记 Shell 是否已准备好处理输入和调整大小
isSuspendedByService?: boolean; // 标记此会话是否已被 SshSuspendService 接管
isMarkedForSuspend?: boolean; // 标记此会话是否已被用户请求挂起(等待断开连接)
suspendLogPath?: string; // 如果标记挂起,则存储日志路径 (基于原始 sessionId)
// suspendLogWritableStream?: NodeJS.WritableStream; // 移除,将直接使用 temporaryLogStorageService.writeToLog
}
+1 -1
View File
@@ -85,7 +85,7 @@ onUnmounted(() => {
});
// *** 新增:计算属性,判断是否在 workspace 路由 ***
// *** 计算属性,判断是否在 workspace 路由 ***
const isWorkspaceRoute = computed(() => route.path === '/workspace');
watch(route, () => {
@@ -193,7 +193,7 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => {
event.preventDefault(); // Prevent default if needed, e.g., form submission
sendCommand(); // Call the existing sendCommand function
} else {
// --- 新增:处理其他按键,取消列表选中状态 ---
// --- 处理其他按键,取消列表选中状态 ---
// 检查按下的键是否是普通输入键或删除键等,而不是导航键或修饰键
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Shift', 'Control', 'Alt', 'Meta', 'Tab', 'Escape'].includes(event.key)) {
const target = commandInputSyncTarget.value;
@@ -22,7 +22,7 @@ const props = defineProps<{
// 定义组件发出的事件 (添加 edit-connection)
const emit = defineEmits(['edit-connection']);
// 新增:用于跟踪每个连接测试状态的响应式对象
// 用于跟踪每个连接测试状态的响应式对象
const testingState = reactive<Record<number, boolean>>({});
// 组件挂载时获取标签列表 (连接列表由父组件传入)
@@ -54,7 +54,7 @@ const getConnectionTagNames = (conn: ConnectionInfo): string[] => {
.filter((name): name is string => !!name); // 过滤掉未找到的标签并确保类型为 string
};
// 新增:计算按标签分组的连接
// 计算按标签分组的连接
const groupedConnections = computed(() => {
const groups: { [key: string]: ConnectionInfo[] } = {};
const untaggedKey = '_untagged_'; // 特殊键,用于未标记的连接
@@ -118,7 +118,7 @@ const formatTimestamp = (timestamp: number | null): string => {
return new Date(timestamp * 1000).toLocaleString(); // 乘以 1000 转换为毫秒
};
// 新增:处理删除连接的方法
// 处理删除连接的方法
const handleDelete = async (conn: ConnectionInfo) => {
// 在函数内部获取 store 实例
const connectionsStore = useConnectionsStore();
@@ -135,7 +135,7 @@ const handleDelete = async (conn: ConnectionInfo) => {
}
};
// 新增:处理测试连接的方法
// 处理测试连接的方法
const handleTestConnection = async (connectionId: number) => {
const connectionsStore = useConnectionsStore(); // 获取 store 实例
testingState[connectionId] = true; // 设置为正在测试状态
@@ -207,10 +207,10 @@ const handleEncodingChange = (event: Event) => {
// const handleCloseContainer = () => { ... };
// const handleMinimizeContainer = () => { ... };
// 新增:Monaco Editor 组件的引用
// Monaco Editor 组件的引用
const monacoEditorRef = ref<InstanceType<typeof MonacoEditor> | null>(null);
// 新增:聚焦活动编辑器的方法
// 聚焦活动编辑器的方法
const focusActiveEditor = (): boolean => {
if (monacoEditorRef.value) {
monacoEditorRef.value.focus();
@@ -219,7 +219,7 @@ const focusActiveEditor = (): boolean => {
return false; // 聚焦失败
};
// 新增:暴露聚焦方法
// 暴露聚焦方法
defineExpose({ focusActiveEditor });
// +++ 注册/注销自定义聚焦动作 +++
@@ -296,7 +296,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
<span v-if="currentTabIsModified" class="modified-indicator">*</span>
</span>
<div class="editor-actions">
<!-- +++ 新增:编码选择下拉菜单 +++ -->
<!-- +++ 编码选择下拉菜单 +++ -->
<div class="encoding-select-wrapper" v-if="activeTab && !currentTabIsLoading">
<select
ref="encodingSelectRef"
@@ -19,7 +19,7 @@ const props = defineProps({
type: Object as PropType<LayoutNode>,
required: true,
},
//
//
isRootRenderer: {
type: Boolean,
default: false,
@@ -30,7 +30,7 @@ const props = defineProps({
required: false, //
default: null, // null
},
// *** props ***
// *** props ***
editorTabs: {
type: Array as PropType<any[]>, // 使 any[]
default: () => [],
@@ -133,6 +133,11 @@
<input type="text" id="telegram-chatid" v-model="telegramConfig.chatId" 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>
<label for="telegram-custom-domain" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramCustomDomain') }}</label>
<input type="url" id="telegram-custom-domain" v-model="telegramConfig.customDomain" placeholder="https://api.example.com"
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>
<label for="telegram-message" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramMessageTemplate') }}</label>
<textarea id="telegram-message" v-model="telegramConfig.messageTemplate" rows="3" :placeholder="`${$t('settings.notifications.form.telegramMessagePlaceholder')} {event}, {timestamp}, {details}.`"
@@ -211,7 +216,8 @@ import {
NotificationEvent,
WebhookConfig,
EmailConfig,
TelegramConfig
TelegramConfig,
NotificationChannelType
} from '../types/server.types';
import { useI18n } from 'vue-i18n';
@@ -305,7 +311,7 @@ const emailConfig = ref<SmtpEmailConfig>({ // Use extended type
smtpPass: '',
from: ''
});
const telegramConfig = ref<TelegramConfig>({ botToken: '', chatId: '', messageTemplate: '' });
const telegramConfig = ref<TelegramConfig>({ botToken: '', chatId: '', messageTemplate: '', customDomain: '' });
const webhookHeadersString = ref('{}'); // For textarea binding
// Watch for initialData changes (when editing)
@@ -331,7 +337,13 @@ watch(() => props.initialData, (newData) => {
from: savedConfig.from || ''
};
} else if (newData.channel_type === 'telegram') {
telegramConfig.value = { ...(newData.config as TelegramConfig) };
const savedConfig = newData.config as TelegramConfig;
telegramConfig.value = {
botToken: savedConfig.botToken || '',
chatId: savedConfig.chatId || '',
messageTemplate: savedConfig.messageTemplate || '',
customDomain: savedConfig.customDomain || '' // Add customDomain
};
}
} else {
// Reset form if initialData becomes null (e.g., switching from edit to add)
@@ -341,7 +353,7 @@ watch(() => props.initialData, (newData) => {
emailConfig.value = {
to: '', bodyTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' // Changed from subjectTemplate
};
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' };
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '', customDomain: '' }; // Add customDomain
webhookHeadersString.value = '{}';
}
headerError.value = null; // Reset header error on data change
@@ -359,9 +371,9 @@ watch(() => formData.channel_type, (newType, oldType) => {
emailConfig.value = {
to: '', bodyTemplate: '', smtpHost: '', smtpPort: 587, smtpSecure: true, smtpUser: '', smtpPass: '', from: '' // Changed from subjectTemplate
};
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '' };
webhookHeadersString.value = '{}';
headerError.value = null;
telegramConfig.value = { botToken: '', chatId: '', messageTemplate: '', customDomain: '' }; // Add customDomain
webhookHeadersString.value = '{}';
headerError.value = null;
testError.value = null;
testResult.value = null;
testingNotification.value = false;
@@ -810,7 +810,7 @@ const formatXtermLabel = (key: keyof ITheme): string => {
return key.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
};
// --- ---
// --- ---
//
const activeThemeName = computed(() => {
@@ -938,7 +938,7 @@ const handleFocusAndSelect = (event: FocusEvent) => {
<main class="flex-grow p-3 md:p-4 md:px-6 overflow-y-auto min-h-0">
<section v-if="currentTab === 'ui'">
<h3 class="mt-0 border-b border-border pb-2 mb-4 text-lg font-semibold text-foreground">{{ t('styleCustomizer.uiStyles') }}</h3>
<!-- 新增主题模式选择 - 小屏幕堆叠 -->
<!-- 主题模式选择 - 小屏幕堆叠 -->
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-start md:items-center gap-2 md:gap-3 mb-6">
<label class="text-left text-foreground text-sm font-medium mb-1 md:mb-0">{{ t('styleCustomizer.themeModeLabel', '主题模式:') }}</label> <!-- TODO: 添加翻译 -->
<div class="flex gap-2 justify-start flex-wrap">
@@ -30,7 +30,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
// --- 拖放状态 Refs ---
// const isDraggingOver = ref(false); // 不再使用,由 showExternalDropOverlay 替代外部拖拽状态
const showExternalDropOverlay = ref(false); // 新增:控制外部文件拖拽蒙版的显示
const showExternalDropOverlay = ref(false); // 控制外部文件拖拽蒙版的显示
const draggedItem = ref<FileListItem | null>(null); // 内部拖拽时,被拖拽的项
const dragOverTarget = ref<string | null>(null); // 内部拖拽时,悬停的目标文件夹名称 (用于行高亮)
const scrollIntervalId = ref<number | null>(null); // 自动滚动计时器 ID
@@ -163,7 +163,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
}
};
// --- 新增:递归遍历文件树的辅助函数 ---
// --- 递归遍历文件树的辅助函数 ---
const traverseFileTree = (item: FileSystemEntry, path = '') => {
path = path || '';
if (item.isFile) {
@@ -192,7 +192,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
// --- 结束新增 ---
// 新增:处理蒙版上的 Drop 事件
// 处理蒙版上的 Drop 事件
const handleOverlayDrop = (event: DragEvent) => {
event.preventDefault(); // 必须阻止,以防浏览器打开文件
// console.log("[DragDrop] Drop event on overlay.");
@@ -30,7 +30,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
// const searchResultCount = ref(0);
// const currentSearchResultIndex = ref(-1);
const terminalOutputBuffer = ref<string[]>([]); // 缓冲 WebSocket 消息直到终端准备好
const isSshConnected = ref(false); // 新增:跟踪 SSH 连接状态
const isSshConnected = ref(false); // 跟踪 SSH 连接状态
// 辅助函数:获取终端消息文本
const getTerminalText = (key: string, params?: Record<string, any>): string => {
@@ -399,7 +399,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
handleTerminalReady,
handleTerminalData, // 这个处理来自 xterm.js 的输入
handleTerminalResize,
sendData, // 新增:允许外部直接发送数据
sendData, // 允许外部直接发送数据
cleanup,
// --- 搜索方法 ---
searchNext,
+1
View File
@@ -602,6 +602,7 @@
"telegramChatId": "Chat ID:",
"telegramMessageTemplate": "Message Template (Optional)",
"telegramMessagePlaceholder": "Default: Markdown format. Use",
"telegramCustomDomain": "Custom Telegram API Domain",
"enabledEvents": "Enabled Events:",
"templateHelp": "Placeholders:",
"invalidJson": "Invalid JSON"
+1
View File
@@ -825,6 +825,7 @@
"telegramChatId": "チャット ID:",
"telegramMessagePlaceholder": "デフォルト: Markdown 形式。利用可能:",
"telegramMessageTemplate": "メッセージテンプレート (オプション)",
"telegramCustomDomain": "カスタム Telegram API ドメイン",
"telegramToken": "ボットトークン:",
"telegramTokenHelp": "安全に保管してください。環境変数の使用をお勧めします。",
"templateHelp": "利用可能なプレースホルダー:",
+1
View File
@@ -600,6 +600,7 @@
"telegramChatId": "聊天 ID:",
"telegramMessageTemplate": "消息模板 (可选)",
"telegramMessagePlaceholder": "默认: Markdown 格式。可使用",
"telegramCustomDomain": "自定义 Telegram API 域名",
"enabledEvents": "启用的事件:",
"templateHelp": "可用占位符:",
"invalidJson": "无效的 JSON 格式"
+5 -5
View File
@@ -16,7 +16,7 @@ const routes: Array<RouteRecordRaw> = [
name: 'Login',
component: () => import('../views/LoginView.vue') // 指向实际的登录组件
},
// 新增:代理管理页面
// 代理管理页面
{
path: '/proxies',
name: 'Proxies',
@@ -35,25 +35,25 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('../views/WorkspaceView.vue'),
// props: true // 不再需要传递 props
},
// 新增:设置页面
// 设置页面
{
path: '/settings',
name: 'Settings',
component: () => import('../views/SettingsView.vue')
},
// 新增:通知管理页面
// 通知管理页面
{
path: '/notifications',
name: 'Notifications',
component: () => import('../views/NotificationsView.vue')
},
// 新增:审计日志页面
// 审计日志页面
{
path: '/audit-logs',
name: 'AuditLogs',
component: () => import('../views/AuditLogView.vue')
},
// 新增:初始设置页面
// 初始设置页面
{
path: '/setup',
name: 'Setup',
@@ -22,7 +22,7 @@ export const useAppearanceStore = defineStore('appearance', () => {
// --- State ---
const isLoading = ref(false);
const error = ref<string | null>(null);
const isStyleCustomizerVisible = ref(false); // 新增:控制样式编辑器可见性
const isStyleCustomizerVisible = ref(false); // 控制样式编辑器可见性
// Appearance Settings State
const appearanceSettings = ref<Partial<AppearanceSettings>>({}); // 从 API 获取的原始设置
+8 -8
View File
@@ -8,7 +8,7 @@ interface UserInfo {
id: number;
username: string;
isTwoFactorEnabled?: boolean; // 后端 /status 接口会返回这个
language?: 'en' | 'zh'; // 新增:用户偏好语言
language?: 'en' | 'zh'; // 用户偏好语言
}
// Passkey Information Interface
@@ -23,7 +23,7 @@ interface PasskeyInfo {
// Add other relevant fields from your backend response
}
// 新增:登录请求的载荷接口
// 登录请求的载荷接口
interface LoginPayload {
username: string;
password: string;
@@ -56,12 +56,12 @@ interface AuthState {
isLoading: boolean;
error: string | null;
loginRequires2FA: boolean; // 新增状态:标记登录是否需要 2FA
// 新增:存储 IP 黑名单数据 (虽然 actions 在这里,但 state 结构保持)
// 存储 IP 黑名单数据 (虽然 actions 在这里,但 state 结构保持)
ipBlacklist: {
entries: any[]; // TODO: Define a proper type for blacklist entries
total: number;
};
needsSetup: boolean; // 新增:是否需要初始设置
needsSetup: boolean; // 是否需要初始设置
publicCaptchaConfig: PublicCaptchaConfig | null; // NEW: Public CAPTCHA config
passkeys: PasskeyInfo[] | null; // NEW: Store for user's passkeys
passkeysLoading: boolean; // NEW: Loading state for passkeys
@@ -87,11 +87,11 @@ export const useAuthStore = defineStore('auth', {
loggedInUser: (state) => state.user?.username,
},
actions: {
// 新增:清除错误状态
// 清除错误状态
clearError() {
this.error = null;
},
// 新增:设置错误状态
// 设置错误状态
setError(errorMessage: string) {
this.error = errorMessage;
},
@@ -196,7 +196,7 @@ export const useAuthStore = defineStore('auth', {
}
},
// 新增:检查并更新认证状态 Action
// 检查并更新认证状态 Action
async checkAuthStatus() {
this.isLoading = true;
try {
@@ -306,7 +306,7 @@ export const useAuthStore = defineStore('auth', {
}
},
// 新增:检查是否需要初始设置
// 检查是否需要初始设置
async checkSetupStatus() {
// 不需要设置 isLoading,这个检查应该在后台快速完成
try {
@@ -31,7 +31,7 @@ import * as editorActions from './session/actions/editorActions';
import * as sftpManagerActions from './session/actions/sftpManagerActions';
import * as modalActions from './session/actions/modalActions';
import * as commandInputActions from './session/actions/commandInputActions';
import * as sshSuspendActions from './session/actions/sshSuspendActions'; // 新增:导入 SSH 挂起 Actions
import * as sshSuspendActions from './session/actions/sshSuspendActions'; // 导入 SSH 挂起 Actions
// 导入需要的类型 (例如 FileInfo 可能会在参数中使用)
import type { FileInfo } from './fileEditor.store';
@@ -13,7 +13,7 @@ import { createWebSocketConnectionManager } from '../../../composables/useWebSoc
import { createSshTerminalManager, type SshTerminalDependencies } from '../../../composables/useSshTerminal';
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../../../composables/useStatusMonitor';
import { createDockerManager, type DockerManagerDependencies } from '../../../composables/useDockerManager';
import { registerSshSuspendHandlers } from './sshSuspendActions'; // 新增:导入 SSH 挂起处理器注册函数
import { registerSshSuspendHandlers } from './sshSuspendActions'; // 导入 SSH 挂起处理器注册函数
// getOrCreateSftpManager 将在 sftpManagerActions.ts 中定义,并在主 store 中协调
// --- 辅助函数 (特定于此模块的 actions) ---
@@ -28,7 +28,7 @@ export const openNewSession = (
connectionsStore: ReturnType<typeof useConnectionsStore>;
t: ReturnType<typeof useI18n>['t'];
},
existingSessionId?: string // 新增:可选的预定义会话 ID
existingSessionId?: string // 可选的预定义会话 ID
) => {
const { connectionsStore, t } = dependencies;
let connInfo: ConnectionInfo | undefined;
@@ -609,7 +609,7 @@ export const useSettingsStore = defineStore('settings', () => {
}
console.log('[SettingsStore] CAPTCHA 设置更新成功。');
// --- 新增:强制 authStore 重新获取配置 ---
// --- 强制 authStore 重新获取配置 ---
console.log('[SettingsStore] Triggering authStore to refetch CAPTCHA config...');
authStore.publicCaptchaConfig = null; // 重置 authStore 的状态以允许重新获取
await authStore.fetchCaptchaConfig(); // 让 authStore 立即获取最新的配置
@@ -11,7 +11,7 @@ export interface AppearanceSettings {
terminalBackgroundImage?: string;
pageBackgroundImage?: string;
editorFontSize?: number;
terminalBackgroundEnabled?: boolean; // 新增:终端背景是否启用
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
}
// 前端用于更新外观设置的数据结构 (对应 API 请求体)
@@ -52,6 +52,7 @@ export interface TelegramConfig {
botToken: string; // Consider masking this in the UI
chatId: string;
messageTemplate?: string;
customDomain?: string; // 允许用户自定义 Telegram API 域名
}
export type NotificationChannelConfig = WebhookConfig | EmailConfig | TelegramConfig;
+1 -1
View File
@@ -18,7 +18,7 @@ const credentials = reactive({
password: '',
});
const twoFactorToken = ref(''); // 2FA
const rememberMe = ref(false); // false
const rememberMe = ref(false); // false
const captchaToken = ref<string | null>(null); // NEW: Store CAPTCHA token
const captchaError = ref<string | null>(null); // NEW: Store CAPTCHA specific error
const hcaptchaWidget = ref<InstanceType<typeof VueHcaptcha> | null>(null); // NEW: Ref for hCaptcha component instance
+2 -2
View File
@@ -819,7 +819,7 @@ const blacklistSettingsForm = reactive({ // Renamed to avoid conflict with store
loginBanDuration: '300', // watcher store
});
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 ipBlacklistEnabled = ref(true); // <-- Local state for IP Blacklist switch
const showConnectionTagsLocal = ref(true); // NEW: Local state for connection tags switch
@@ -944,7 +944,7 @@ watch(settings, (newSettings, oldSettings) => {
dockerInterval.value = parseInt(newSettings.dockerStatusIntervalSeconds || '2', 10); // Docker
dockerExpandDefault.value = dockerDefaultExpandBoolean.value; // Docker
statusMonitorIntervalLocal.value = statusMonitorIntervalSecondsNumber.value; //
workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; //
workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; //
commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target
selectedTimezone.value = newSettings.timezone || 'UTC'; //
ipBlacklistEnabled.value = ipBlacklistEnabledBoolean.value; // <-- Sync IP Blacklist enabled state
+4 -4
View File
@@ -88,11 +88,11 @@ import { ref } from 'vue';
import apiClient from '../utils/apiClient'; // 使 apiClient
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../stores/auth.store'; // *** Auth Store ***
import { useAuthStore } from '../stores/auth.store'; // *** Auth Store ***
const { t } = useI18n();
const router = useRouter();
const authStore = useAuthStore(); // *** Auth Store ***
const authStore = useAuthStore(); // *** Auth Store ***
const username = ref('');
const password = ref('');
@@ -125,9 +125,9 @@ const handleSetup = async () => {
confirmPassword: confirmPassword.value
});
successMessage.value = t('setup.success');
// *** needsSetup ***
// *** needsSetup ***
authStore.needsSetup = false;
// *** ***
// *** ***
authStore.isAuthenticated = false;
authStore.user = null;
//