update
This commit is contained in:
@@ -62,7 +62,7 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
ipBlacklistService.recordFailedAttempt(clientIp);
|
ipBlacklistService.recordFailedAttempt(clientIp);
|
||||||
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp });
|
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp });
|
||||||
notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp });
|
// notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp }); // 保留原有调用,因为这里已经有了
|
||||||
res.status(401).json({ message: 'CAPTCHA 验证失败。' });
|
res.status(401).json({ message: 'CAPTCHA 验证失败。' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,8 +89,8 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
ipBlacklistService.recordFailedAttempt(clientIp);
|
ipBlacklistService.recordFailedAttempt(clientIp);
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'User not found', ip: clientIp });
|
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'User not found', ip: clientIp });
|
||||||
// 发送登录失败通知
|
// 发送登录失败通知 (保留原有调用)
|
||||||
notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'User not found', ip: clientIp });
|
// notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'User not found', ip: clientIp });
|
||||||
res.status(401).json({ message: '无效的凭据。' });
|
res.status(401).json({ message: '无效的凭据。' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,8 +104,8 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
ipBlacklistService.recordFailedAttempt(clientIp);
|
ipBlacklistService.recordFailedAttempt(clientIp);
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'Invalid password', ip: clientIp });
|
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'Invalid password', ip: clientIp });
|
||||||
// 发送登录失败通知
|
// 发送登录失败通知 (保留原有调用)
|
||||||
notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'Invalid password', ip: clientIp });
|
// notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'Invalid password', ip: clientIp });
|
||||||
res.status(401).json({ message: '无效的凭据。' });
|
res.status(401).json({ message: '无效的凭据。' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,6 +126,7 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
|||||||
ipBlacklistService.resetAttempts(clientIp);
|
ipBlacklistService.resetAttempts(clientIp);
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('LOGIN_SUCCESS', { userId: user.id, username, ip: clientIp });
|
auditLogService.logAction('LOGIN_SUCCESS', { userId: user.id, username, ip: clientIp });
|
||||||
|
notificationService.sendNotification('LOGIN_SUCCESS', { userId: user.id, username, ip: clientIp }); // 添加通知调用
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
req.session.username = user.username;
|
req.session.username = user.username;
|
||||||
req.session.requiresTwoFactor = false; // 明确标记不需要 2FA
|
req.session.requiresTwoFactor = false; // 明确标记不需要 2FA
|
||||||
@@ -235,6 +236,7 @@ export const verifyLogin2FA = async (req: Request, res: Response): Promise<void>
|
|||||||
ipBlacklistService.resetAttempts(clientIp);
|
ipBlacklistService.resetAttempts(clientIp);
|
||||||
// 记录审计日志 (2FA 成功也算登录成功) (添加 IP)
|
// 记录审计日志 (2FA 成功也算登录成功) (添加 IP)
|
||||||
auditLogService.logAction('LOGIN_SUCCESS', { userId: user.id, username: user.username, ip: clientIp, twoFactor: true });
|
auditLogService.logAction('LOGIN_SUCCESS', { userId: user.id, username: user.username, ip: clientIp, twoFactor: true });
|
||||||
|
notificationService.sendNotification('LOGIN_SUCCESS', { userId: user.id, username: user.username, ip: clientIp, twoFactor: true }); // 添加通知调用
|
||||||
// 验证成功,建立完整会话
|
// 验证成功,建立完整会话
|
||||||
req.session.username = user.username;
|
req.session.username = user.username;
|
||||||
req.session.requiresTwoFactor = false; // 标记 2FA 已完成
|
req.session.requiresTwoFactor = false; // 标记 2FA 已完成
|
||||||
@@ -261,6 +263,7 @@ export const verifyLogin2FA = async (req: Request, res: Response): Promise<void>
|
|||||||
ipBlacklistService.recordFailedAttempt(clientIp);
|
ipBlacklistService.recordFailedAttempt(clientIp);
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('LOGIN_FAILURE', { userId: user.id, username: user.username, reason: 'Invalid 2FA token', ip: clientIp });
|
auditLogService.logAction('LOGIN_FAILURE', { userId: user.id, username: user.username, reason: 'Invalid 2FA token', ip: clientIp });
|
||||||
|
notificationService.sendNotification('LOGIN_FAILURE', { userId: user.id, username: user.username, reason: 'Invalid 2FA token', ip: clientIp }); // 添加通知调用
|
||||||
res.status(401).json({ message: '验证码无效。' });
|
res.status(401).json({ message: '验证码无效。' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +339,7 @@ export const changePassword = async (req: Request, res: Response): Promise<void>
|
|||||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
|
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('PASSWORD_CHANGED', { userId, ip: clientIp });
|
auditLogService.logAction('PASSWORD_CHANGED', { userId, ip: clientIp });
|
||||||
|
notificationService.sendNotification('PASSWORD_CHANGED', { userId, ip: clientIp }); // 添加通知调用
|
||||||
|
|
||||||
res.status(200).json({ message: '密码已成功修改。' });
|
res.status(200).json({ message: '密码已成功修改。' });
|
||||||
|
|
||||||
@@ -470,6 +474,7 @@ export const verifyPasskeyRegistration = async (req: Request, res: Response): Pr
|
|||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
const regInfo: any = verification.registrationInfo;
|
const regInfo: any = verification.registrationInfo;
|
||||||
auditLogService.logAction('PASSKEY_REGISTERED', { userId, passkeyId: regInfo.credentialID, name, ip: clientIp });
|
auditLogService.logAction('PASSKEY_REGISTERED', { userId, passkeyId: regInfo.credentialID, name, ip: clientIp });
|
||||||
|
notificationService.sendNotification('PASSKEY_REGISTERED', { userId, passkeyId: regInfo.credentialID, name, ip: clientIp }); // 添加通知调用
|
||||||
res.status(201).json({ message: 'Passkey 注册成功!', verified: true });
|
res.status(201).json({ message: 'Passkey 注册成功!', verified: true });
|
||||||
} else {
|
} else {
|
||||||
console.error(`用户 ${userId} Passkey 注册验证失败:`, verification);
|
console.error(`用户 ${userId} Passkey 注册验证失败:`, verification);
|
||||||
@@ -531,6 +536,7 @@ export const verifyAndActivate2FA = async (req: Request, res: Response): Promise
|
|||||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('2FA_ENABLED', { userId, ip: clientIp });
|
auditLogService.logAction('2FA_ENABLED', { userId, ip: clientIp });
|
||||||
|
notificationService.sendNotification('2FA_ENABLED', { userId, ip: clientIp }); // 添加通知调用
|
||||||
|
|
||||||
// 清除 session 中的临时密钥
|
// 清除 session 中的临时密钥
|
||||||
delete req.session.tempTwoFactorSecret;
|
delete req.session.tempTwoFactorSecret;
|
||||||
@@ -594,6 +600,7 @@ export const disable2FA = async (req: Request, res: Response): Promise<void> =>
|
|||||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('2FA_DISABLED', { userId, ip: clientIp });
|
auditLogService.logAction('2FA_DISABLED', { userId, ip: clientIp });
|
||||||
|
notificationService.sendNotification('2FA_DISABLED', { userId, ip: clientIp }); // 添加通知调用
|
||||||
|
|
||||||
res.status(200).json({ message: '两步验证已成功禁用。' });
|
res.status(200).json({ message: '两步验证已成功禁用。' });
|
||||||
|
|
||||||
@@ -679,6 +686,7 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
|
|||||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
// 记录审计日志 (添加 IP)
|
// 记录审计日志 (添加 IP)
|
||||||
auditLogService.logAction('ADMIN_SETUP_COMPLETE', { userId: newUser.id, username, ip: clientIp });
|
auditLogService.logAction('ADMIN_SETUP_COMPLETE', { userId: newUser.id, username, ip: clientIp });
|
||||||
|
notificationService.sendNotification('ADMIN_SETUP_COMPLETE', { userId: newUser.id, username, ip: clientIp }); // 添加通知调用
|
||||||
|
|
||||||
res.status(201).json({ message: '初始管理员账号创建成功!' });
|
res.status(201).json({ message: '初始管理员账号创建成功!' });
|
||||||
|
|
||||||
@@ -708,6 +716,7 @@ export const logout = (req: Request, res: Response): void => {
|
|||||||
if (userId) { // 仅在能获取到 userId 时记录
|
if (userId) { // 仅在能获取到 userId 时记录
|
||||||
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
auditLogService.logAction('LOGOUT', { userId, username, ip: clientIp });
|
auditLogService.logAction('LOGOUT', { userId, username, ip: clientIp });
|
||||||
|
notificationService.sendNotification('LOGOUT', { userId, username, ip: clientIp }); // 添加通知调用
|
||||||
}
|
}
|
||||||
res.status(200).json({ message: '已成功登出。' });
|
res.status(200).json({ message: '已成功登出。' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"testNotification": {
|
"testNotification": {
|
||||||
"subject": "Nexus Terminal Test Notification ({eventDisplay})",
|
"subject": "Nexus Terminal Test Notification ({event})",
|
||||||
"email": {
|
"email": {
|
||||||
"body": "This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}",
|
"body": "This is a test email from Nexus Terminal for event '{{event}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}",
|
||||||
"bodyHtml": "<p>This is a test email from <b>Nexus Terminal</b> for event '{{eventDisplay}}'.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: {{timestamp}}</p>"
|
"bodyHtml": "<p>This is a test email from <b>Nexus Terminal</b> for event '{{event}}'.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: {{timestamp}}</p>"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"detailsMessage": "This is a test notification from Nexus Terminal (Webhook - i18n) for event '{{eventDisplay}}'."
|
"detailsMessage": "This is a test notification from Nexus Terminal (Webhook - i18n) for event '{{event}}'."
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"detailsMessage": "This is a test notification from Nexus Terminal (Telegram - i18n) for event '{{eventDisplay}}'.",
|
"detailsMessage": "This is a test notification from Nexus Terminal (Telegram - i18n) for event '{{event}}'.",
|
||||||
"bodyTemplate": "*Nexus Terminal Test Notification*\nEvent: `{eventDisplay}`\nTimestamp: {timestamp}\nDetails:\n```\n{details}\n```"
|
"bodyTemplate": "*Nexus Terminal Test Notification*\nEvent: `{event}`\nTimestamp: {timestamp}\nDetails:\n```\n{details}\n```"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eventDisplay": {
|
"event": {
|
||||||
"LOGIN_SUCCESS": "Login Success",
|
"LOGIN_SUCCESS": "Login Success",
|
||||||
"LOGIN_FAILURE": "Login Failure",
|
"LOGIN_FAILURE": "Login Failure",
|
||||||
"LOGOUT": "Logout",
|
"LOGOUT": "Logout",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
},
|
},
|
||||||
"eventBody": {
|
"eventBody": {
|
||||||
|
|
||||||
"SETTINGS_UPDATED": "Event: {{eventDisplay}}\nTimestamp: {{timestamp}}\nDetails:\n{{details}}"
|
"SETTINGS_UPDATED": "Event: {{event}}\nTimestamp: {{timestamp}}\nDetails:\n{{details}}"
|
||||||
},
|
},
|
||||||
"connection": {
|
"connection": {
|
||||||
"testSuccess": "Connection test successful for '{{name}}'!",
|
"testSuccess": "Connection test successful for '{{name}}'!",
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"testNotification": {
|
"testNotification": {
|
||||||
"subject": "星枢ターミナル テスト通知 ({eventDisplay})",
|
"subject": "星枢ターミナル テスト通知 ({event})",
|
||||||
"email": {
|
"email": {
|
||||||
"body": "これは、星枢ターミナルからのイベント'{{eventDisplay}}'に関するテストメールです。\n\nこのメールを受信した場合、SMTP 設定は正常に機能しています。\n\nタイムスタンプ: {{timestamp}}",
|
"body": "これは、星枢ターミナルからのイベント'{{event}}'に関するテストメールです。\n\nこのメールを受信した場合、SMTP 設定は正常に機能しています。\n\nタイムスタンプ: {{timestamp}}",
|
||||||
"bodyHtml": "<p>これは、<b>星枢ターミナル</b>からのイベント'{{eventDisplay}}'に関するテストメールです。</p><p>このメールを受信した場合、SMTP 設定は正常に機能しています。</p><p>タイムスタンプ: {{timestamp}}</p>"
|
"bodyHtml": "<p>これは、<b>星枢ターミナル</b>からのイベント'{{event}}'に関するテストメールです。</p><p>このメールを受信した場合、SMTP 設定は正常に機能しています。</p><p>タイムスタンプ: {{timestamp}}</p>"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"detailsMessage": "これは星枢ターミナルからのテスト通知 (Webhook - i18n) です。 イベント:'{{eventDisplay}}'。"
|
"detailsMessage": "これは星枢ターミナルからのテスト通知 (Webhook - i18n) です。 イベント:'{{event}}'。"
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"detailsMessage": "これは星枢ターミナルからのテスト通知 (Telegram - i18n) です。 イベント:'{{eventDisplay}}'。",
|
"detailsMessage": "これは星枢ターミナルからのテスト通知 (Telegram - i18n) です。 イベント:'{{event}}'。",
|
||||||
"bodyTemplate": "*星枢ターミナル テスト通知*\nイベント: `{eventDisplay}`\nタイムスタンプ: {timestamp}\n詳細:\n```\n{details}\n```"
|
"bodyTemplate": "*星枢ターミナル テスト通知*\nイベント: `{event}`\nタイムスタンプ: {timestamp}\n詳細:\n```\n{details}\n```"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eventDisplay": {
|
"event": {
|
||||||
"LOGIN_SUCCESS": "ログイン成功",
|
"LOGIN_SUCCESS": "ログイン成功",
|
||||||
"LOGIN_FAILURE": "ログイン失敗",
|
"LOGIN_FAILURE": "ログイン失敗",
|
||||||
"LOGOUT": "ログアウト",
|
"LOGOUT": "ログアウト",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"ADMIN_SETUP_COMPLETE": "初期管理者設定完了"
|
"ADMIN_SETUP_COMPLETE": "初期管理者設定完了"
|
||||||
},
|
},
|
||||||
"eventBody": {
|
"eventBody": {
|
||||||
"SETTINGS_UPDATED": "イベント: {{eventDisplay}}\nタイムスタンプ: {{timestamp}}\n詳細:\n{{details}}"
|
"SETTINGS_UPDATED": "イベント: {{event}}\nタイムスタンプ: {{timestamp}}\n詳細:\n{{details}}"
|
||||||
},
|
},
|
||||||
"connection": {
|
"connection": {
|
||||||
"testSuccess": "接続 '{{name}}' のテストに成功しました!",
|
"testSuccess": "接続 '{{name}}' のテストに成功しました!",
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"testNotification": {
|
"testNotification": {
|
||||||
"subject": "星枢终端测试通知 ({eventDisplay})",
|
"subject": "星枢终端测试通知 ({event})",
|
||||||
"email": {
|
"email": {
|
||||||
"body": "这是一封来自星枢终端关于事件 '{{eventDisplay}}' 的测试邮件。\n\n如果您收到此邮件,表示您的 SMTP 配置工作正常。\n\n时间戳: {{timestamp}}",
|
"body": "这是一封来自星枢终端关于事件 '{{event}}' 的测试邮件。\n\n如果您收到此邮件,表示您的 SMTP 配置工作正常。\n\n时间戳: {{timestamp}}",
|
||||||
"bodyHtml": "<p>这是一封来自 <b>星枢终端</b> 关于事件 '{{eventDisplay}}' 的测试邮件。</p><p>如果您收到此邮件,表示您的 SMTP 配置工作正常。</p><p>时间戳: {{timestamp}}</p>"
|
"bodyHtml": "<p>这是一封来自 <b>星枢终端</b> 关于事件 '{{event}}' 的测试邮件。</p><p>如果您收到此邮件,表示您的 SMTP 配置工作正常。</p><p>时间戳: {{timestamp}}</p>"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"detailsMessage": "这是一条来自星枢终端的测试通知 (Webhook - i18n),事件:'{{eventDisplay}}'。"
|
"detailsMessage": "这是一条来自星枢终端的测试通知 (Webhook - i18n),事件:'{{event}}'。"
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"detailsMessage": "这是一条来自星枢终端的测试通知 (Telegram - i18n),事件:'{{eventDisplay}}'。",
|
"detailsMessage": "这是一条来自星枢终端的测试通知 (Telegram - i18n),事件:'{{event}}'。",
|
||||||
"bodyTemplate": "*星枢终端测试通知*\n事件: `{eventDisplay}`\n时间戳: {timestamp}\n详情:\n```\n{details}\n```"
|
"bodyTemplate": "*星枢终端测试通知*\n事件: `{event}`\n时间戳: {timestamp}\n详情:\n```\n{details}\n```"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eventDisplay": {
|
"event": {
|
||||||
"LOGIN_SUCCESS": "登录成功",
|
"LOGIN_SUCCESS": "登录成功",
|
||||||
"LOGIN_FAILURE": "登录失败",
|
"LOGIN_FAILURE": "登录失败",
|
||||||
"LOGOUT": "登出",
|
"LOGOUT": "登出",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
},
|
},
|
||||||
"eventBody": {
|
"eventBody": {
|
||||||
|
|
||||||
"SETTINGS_UPDATED": "事件: {{eventDisplay}}\n时间戳: {{timestamp}}\n详情:\n{{details}}"
|
"SETTINGS_UPDATED": "事件: {{event}}\n时间戳: {{timestamp}}\n详情:\n{{details}}"
|
||||||
},
|
},
|
||||||
"connection": {
|
"connection": {
|
||||||
"testSuccess": "连接 '{{name}}' 测试成功!",
|
"testSuccess": "连接 '{{name}}' 测试成功!",
|
||||||
|
|||||||
@@ -95,14 +95,14 @@ export class NotificationService {
|
|||||||
|
|
||||||
const transporter = nodemailer.createTransport(transporterOptions);
|
const transporter = nodemailer.createTransport(transporterOptions);
|
||||||
|
|
||||||
const eventDisplayName = i18next.t(`eventDisplay.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' });
|
const eventDisplayName = i18next.t(`event.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' });
|
||||||
|
|
||||||
const mailOptions: Mail.Options = {
|
const mailOptions: Mail.Options = {
|
||||||
from: config.from,
|
from: config.from,
|
||||||
to: config.to,
|
to: config.to,
|
||||||
subject: i18next.t(testSubjectKey, { lng: userLang, defaultValue: 'Nexus Terminal Test Notification ({eventDisplay})', eventDisplay: eventDisplayName }),
|
subject: i18next.t(testSubjectKey, { lng: userLang, defaultValue: 'Nexus Terminal Test Notification ({event})', eventDisplay: eventDisplayName }),
|
||||||
text: i18next.t(testEmailBodyKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, eventDisplay: eventDisplayName }),
|
text: i18next.t(testEmailBodyKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{event}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, eventDisplay: eventDisplayName }),
|
||||||
html: i18next.t(testEmailBodyHtmlKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `<p>This is a test email from <b>Nexus Terminal</b> for event '{{eventDisplay}}'.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: {{timestamp}}</p>`, eventDisplay: eventDisplayName }),
|
html: i18next.t(testEmailBodyHtmlKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `<p>This is a test email from <b>Nexus Terminal</b> for event '{{event}}'.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: {{timestamp}}</p>`, eventDisplay: eventDisplayName }),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -142,10 +142,22 @@ export class NotificationService {
|
|||||||
const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details 不是带有 message 属性的对象';
|
const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details 不是带有 message 属性的对象';
|
||||||
console.log(`[通知测试 - Webhook] 测试负载已创建。翻译后的 details.message:`, translatedWebhookMessage);
|
console.log(`[通知测试 - Webhook] 测试负载已创建。翻译后的 details.message:`, translatedWebhookMessage);
|
||||||
|
|
||||||
const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event });
|
const eventDisplayName = i18next.t(`event.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event });
|
||||||
const defaultBody = JSON.stringify(testPayload, null, 2);
|
const defaultBody = JSON.stringify(testPayload, null, 2);
|
||||||
const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`;
|
const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`;
|
||||||
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, testPayload, defaultBody, eventDisplayName);
|
// Prepare data object for _renderTemplate
|
||||||
|
const templateDataWebhookTest: Record<string, string> = {
|
||||||
|
event: testPayload.event,
|
||||||
|
eventDisplay: eventDisplayName, // Assuming no markdown needed for webhook test
|
||||||
|
timestamp: new Date(testPayload.timestamp).toISOString(),
|
||||||
|
// Use the message from details if available
|
||||||
|
details: (typeof testPayload.details === 'object' && testPayload.details?.message)
|
||||||
|
? testPayload.details.message
|
||||||
|
: (typeof testPayload.details === 'string'
|
||||||
|
? testPayload.details
|
||||||
|
: JSON.stringify(testPayload.details || {}, null, 2))
|
||||||
|
};
|
||||||
|
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, templateDataWebhookTest, defaultBody); // Use new signature (3 args)
|
||||||
|
|
||||||
const requestConfig: AxiosRequestConfig = {
|
const requestConfig: AxiosRequestConfig = {
|
||||||
method: config.method || 'POST',
|
method: config.method || 'POST',
|
||||||
@@ -214,9 +226,19 @@ export class NotificationService {
|
|||||||
const templateToUse = config.messageTemplate || defaultMessageTemplateFromI18n;
|
const templateToUse = config.messageTemplate || defaultMessageTemplateFromI18n;
|
||||||
console.log(`[通知测试 - Telegram] 要渲染的模板:`, templateToUse);
|
console.log(`[通知测试 - Telegram] 要渲染的模板:`, templateToUse);
|
||||||
|
|
||||||
const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event });
|
const eventDisplayName = i18next.t(`event.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event });
|
||||||
const messageText = this._renderTemplate(templateToUse, testPayload, '', eventDisplayName);
|
// Prepare data object for _renderTemplate
|
||||||
console.log(`[通知测试 - Telegram] 渲染的消息文本:`, messageText);
|
const templateDataTelegramTest: Record<string, string> = {
|
||||||
|
// Use 'event' key with escaped raw event name
|
||||||
|
event: this._escapeBasicMarkdown(testPayload.event),
|
||||||
|
eventDisplay: this._escapeBasicMarkdown(eventDisplayName), // Keep escaped display name too
|
||||||
|
timestamp: new Date(testPayload.timestamp).toISOString(),
|
||||||
|
// Use 'details' key with escaped formatted details message
|
||||||
|
details: this._escapeBasicMarkdown(messageFromPayload)
|
||||||
|
};
|
||||||
|
// Render using the chosen template and prepared data
|
||||||
|
const messageText = this._renderTemplate(templateToUse, templateDataTelegramTest, defaultMessageTemplateFromI18n); // Use new signature (3 args)
|
||||||
|
console.log(`[通知测试 - Telegram] 渲染的消息文本:`, messageText); // Keep original log message
|
||||||
|
|
||||||
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
||||||
|
|
||||||
@@ -294,14 +316,23 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private _renderTemplate(template: string | undefined, payload: NotificationPayload, defaultText: string, eventDisplayName?: string): string {
|
// Helper function to escape basic Markdown characters (`*`, `_`, `` ` ``, `[`)
|
||||||
|
private _escapeBasicMarkdown(text: string): string {
|
||||||
|
if (typeof text !== 'string') return '';
|
||||||
|
// Escape *, _, `, [
|
||||||
|
// Note: Telegram's Markdown parser might have quirks.
|
||||||
|
// If issues persist, consider escaping more characters or using MarkdownV2 with its stricter rules.
|
||||||
|
return text.replace(/([*_`\[])/g, '\\$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderTemplate(template: string | undefined, data: Record<string, string>, defaultText: string): string {
|
||||||
if (!template) return defaultText;
|
if (!template) return defaultText;
|
||||||
let rendered = template;
|
let rendered = template;
|
||||||
rendered = rendered.replace(/\{event\}/g, payload.event);
|
for (const key in data) {
|
||||||
rendered = rendered.replace(/\{eventDisplay\}/g, eventDisplayName || payload.event);
|
// Replace placeholders like {key} with data[key]
|
||||||
rendered = rendered.replace(/\{timestamp\}/g, new Date(payload.timestamp).toISOString());
|
// Assumes data values are already properly escaped if needed
|
||||||
const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2);
|
rendered = rendered.replace(new RegExp(`\\{${key}\\}`, 'g'), data[key]);
|
||||||
rendered = rendered.replace(/\{details\}/g, detailsString);
|
}
|
||||||
return rendered;
|
return rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,14 +343,26 @@ export class NotificationService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
||||||
|
|
||||||
const translatedDetails = this._translatePayloadDetails(payload.details, userLang);
|
const translatedDetails = this._translatePayloadDetails(payload.details, userLang);
|
||||||
const translatedPayload = { ...payload, details: translatedDetails };
|
const translatedPayload = { ...payload, details: translatedDetails };
|
||||||
|
|
||||||
const defaultBody = JSON.stringify(translatedPayload, null, 2);
|
const defaultBody = JSON.stringify(translatedPayload, null, 2);
|
||||||
const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`;
|
const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`;
|
||||||
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, translatedPayload, defaultBody, eventDisplayName);
|
// Prepare data object for _renderTemplate
|
||||||
|
const templateDataWebhook: Record<string, string> = {
|
||||||
|
event: translatedPayload.event,
|
||||||
|
eventDisplay: eventDisplayName, // Assuming no markdown needed for webhook
|
||||||
|
timestamp: new Date(translatedPayload.timestamp).toISOString(),
|
||||||
|
// Use the translated message if available, otherwise stringify
|
||||||
|
details: (typeof translatedPayload.details === 'object' && translatedPayload.details?.message)
|
||||||
|
? translatedPayload.details.message
|
||||||
|
: (typeof translatedPayload.details === 'string'
|
||||||
|
? translatedPayload.details
|
||||||
|
: JSON.stringify(translatedPayload.details || {}, null, 2))
|
||||||
|
};
|
||||||
|
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, templateDataWebhook, defaultBody); // Use new signature (3 args)
|
||||||
|
|
||||||
const requestConfig: AxiosRequestConfig = {
|
const requestConfig: AxiosRequestConfig = {
|
||||||
method: config.method || 'POST',
|
method: config.method || 'POST',
|
||||||
@@ -368,15 +411,29 @@ export class NotificationService {
|
|||||||
i18nOptions.details = payload.details;
|
i18nOptions.details = payload.details;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
||||||
|
|
||||||
const defaultSubjectKey = `event.${payload.event}`;
|
const defaultSubjectKey = `event.${payload.event}`;
|
||||||
const defaultSubjectFallback = `Nexus Terminal Notification: {eventDisplay}`;
|
const defaultSubjectFallback = `Nexus Terminal Notification: {event}`;
|
||||||
const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
|
const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
|
||||||
|
|
||||||
const defaultSubjectTemplateKey = 'testNotification.subject';
|
const defaultSubjectTemplateKey = 'testNotification.subject';
|
||||||
const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
|
const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
|
||||||
const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, payload, subjectText, eventDisplayName);
|
// Prepare data object for _renderTemplate (for subject)
|
||||||
|
const templateDataEmailSubject: Record<string, string> = {
|
||||||
|
event: payload.event,
|
||||||
|
eventDisplay: eventDisplayName, // Assuming subject doesn't need markdown
|
||||||
|
timestamp: new Date(payload.timestamp).toISOString(),
|
||||||
|
details: typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2),
|
||||||
|
// Add other relevant fields from i18nOptions if needed by subject template
|
||||||
|
...Object.entries(i18nOptions).reduce((acc, [key, value]) => {
|
||||||
|
if (key !== 'lng') { // Exclude 'lng' itself
|
||||||
|
acc[key] = String(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
};
|
||||||
|
const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, templateDataEmailSubject, subjectText); // Use new signature (3 args)
|
||||||
|
|
||||||
|
|
||||||
const bodyKey = `eventBody.${payload.event}`;
|
const bodyKey = `eventBody.${payload.event}`;
|
||||||
@@ -401,38 +458,79 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> {
|
private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> {
|
||||||
|
console.log(`[_sendTelegram] Initiating for event: ${payload.event}, Setting ID: ${setting.id}, Lang: ${userLang}`);
|
||||||
|
console.log(`[_sendTelegram] Received payload:`, JSON.stringify(payload, null, 2));
|
||||||
const config = setting.config as TelegramConfig;
|
const config = setting.config as TelegramConfig;
|
||||||
if (!config.botToken || !config.chatId) {
|
if (!config.botToken || !config.chatId) {
|
||||||
console.error(`[通知] Telegram 设置 ID ${setting.id} 缺少 botToken 或 chatId。`);
|
console.error(`[通知] Telegram 设置 ID ${setting.id} 缺少 botToken 或 chatId。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18nOptions: Record<string, any> = { lng: userLang };
|
let detailsText = '';
|
||||||
if (payload.details && typeof payload.details === 'object') {
|
if (payload.details) {
|
||||||
Object.assign(i18nOptions, payload.details);
|
if (payload.event === 'SETTINGS_UPDATED' && typeof payload.details === 'object' && Array.isArray(payload.details.updatedKeys)) {
|
||||||
} else if (payload.details !== undefined) {
|
detailsText = payload.details.updatedKeys.join(', ');
|
||||||
i18nOptions.details = payload.details;
|
|
||||||
|
} else if (typeof payload.details === 'string') {
|
||||||
|
detailsText = payload.details;
|
||||||
|
} else {
|
||||||
|
// Generic fallback for unhandled object details
|
||||||
|
detailsText = JSON.stringify(payload.details);
|
||||||
}
|
}
|
||||||
const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
}
|
||||||
|
console.log(`[_sendTelegram] Formatted detailsText:`, detailsText);
|
||||||
|
|
||||||
const messageKey = `eventBody.${payload.event}`;
|
// 3. Prepare data for template placeholders AND i18n interpolation (NO escaping here)
|
||||||
const detailsStr = payload.details ? `\nDetails: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : '';
|
// Get the translated event name using the correct key 'event.<EVENT_NAME>'
|
||||||
const defaultMessageTemplateFallback = `*Nexus Terminal Notification*\n\nEvent: \`{eventDisplay}\`\nTimestamp: {timestamp}${detailsStr}`;
|
const translatedEventName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event });
|
||||||
const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageTemplateFallback, eventDisplay: eventDisplayName });
|
|
||||||
|
|
||||||
const defaultTemplateKey = `notifications:${testTelegramBodyTemplateKey}`;
|
const templateData: Record<string, string> = {
|
||||||
const defaultMessageTemplateFromI18n = i18next.t(defaultTemplateKey, { lng: userLang, defaultValue: translatedBody, eventDisplay: eventDisplayName });
|
// Assign the *translated* event name to the 'event' key (NO escaping)
|
||||||
|
event: translatedEventName,
|
||||||
|
// ISO timestamp (usually safe)
|
||||||
|
timestamp: new Date(payload.timestamp).toISOString(),
|
||||||
|
// Formatted details string (NO escaping)
|
||||||
|
details: detailsText
|
||||||
|
// Note: We no longer create eventDisplay key
|
||||||
|
};
|
||||||
|
console.log(`[_sendTelegram] Prepared templateData (NO escaping):`, JSON.stringify(templateData, null, 2));
|
||||||
|
|
||||||
const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplateFromI18n, payload, translatedBody, eventDisplayName);
|
// 4. Handle template
|
||||||
|
let messageText = '';
|
||||||
|
if (config.messageTemplate) {
|
||||||
|
// User has a custom template, render it using prepared data
|
||||||
|
console.log(`[_sendTelegram] Using custom template:`, config.messageTemplate);
|
||||||
|
// Provide a fallback text in case rendering fails
|
||||||
|
const fallbackForCustom = `Event: ${templateData.event}, Details: ${templateData.details}`; // Use 'event' key now
|
||||||
|
messageText = this._renderTemplate(config.messageTemplate, templateData, fallbackForCustom);
|
||||||
|
} else {
|
||||||
|
// No custom template, use i18n to generate the full body
|
||||||
|
const i18nKey = `eventBody.${payload.event}`;
|
||||||
|
console.log(`[_sendTelegram] Using i18n template key:`, i18nKey);
|
||||||
|
// Define a fallback structure using the prepared data (NO escaping)
|
||||||
|
const fallbackBody = `*Fallback Notification*\nEvent: ${templateData.event}\nTime: \`${templateData.timestamp}\`\nDetails: ${templateData.details}`; // Use 'event' key now
|
||||||
|
// Pass the prepared data for interpolation within the i18n translation
|
||||||
|
messageText = i18next.t(i18nKey, {
|
||||||
|
lng: userLang,
|
||||||
|
...templateData, // Pass all prepared data for interpolation (event, timestamp, details)
|
||||||
|
// If specific i18n keys need raw data (like the keys array), add them here:
|
||||||
|
// updatedKeys: (payload.event === 'SETTINGS_UPDATED' && Array.isArray(payload.details?.updatedKeys)) ? payload.details.updatedKeys.join(', ') : '',
|
||||||
|
defaultValue: fallbackBody
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`[_sendTelegram] Final message text to send:`, messageText);
|
||||||
|
|
||||||
|
// 6. Send to Telegram (Moved step number)
|
||||||
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[通知] 发送 Telegram 消息到聊天 ID ${config.chatId} (事件: ${payload.event})`);
|
console.log(`[通知] 发送 Telegram 消息到聊天 ID ${config.chatId} (事件: ${payload.event})`);
|
||||||
const response = await axios.post(telegramApiUrl, {
|
const requestBody = {
|
||||||
chat_id: config.chatId,
|
chat_id: config.chatId,
|
||||||
text: messageText,
|
text: messageText,
|
||||||
parse_mode: 'Markdown',
|
parse_mode: 'Markdown', // Use standard Markdown
|
||||||
}, { timeout: 10000 });
|
};
|
||||||
|
console.log(`[_sendTelegram] Sending request to Telegram API:`, JSON.stringify(requestBody, null, 2));
|
||||||
|
const response = await axios.post(telegramApiUrl, requestBody, { timeout: 10000 });
|
||||||
console.log(`[通知] Telegram 消息发送成功。响应 OK:`, response.data?.ok);
|
console.log(`[通知] Telegram 消息发送成功。响应 OK:`, response.data?.ok);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = error.response?.data?.description || error.response?.data || error.message;
|
const errorMessage = error.response?.data?.description || error.response?.data || error.message;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { settingsService } from '../services/settings.service';
|
import { settingsService } from '../services/settings.service';
|
||||||
import { AuditLogService } from '../services/audit.service';
|
import { AuditLogService } from '../services/audit.service';
|
||||||
|
import { NotificationService } from '../services/notification.service'; // 添加导入
|
||||||
import { ipBlacklistService } from '../services/ip-blacklist.service';
|
import { ipBlacklistService } from '../services/ip-blacklist.service';
|
||||||
import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types'; // <-- Import CAPTCHA types
|
import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types'; // <-- Import CAPTCHA types
|
||||||
|
|
||||||
const auditLogService = new AuditLogService();
|
const auditLogService = new AuditLogService();
|
||||||
|
const notificationService = new NotificationService(); // 添加实例
|
||||||
|
|
||||||
export const settingsController = {
|
export const settingsController = {
|
||||||
/**
|
/**
|
||||||
@@ -59,6 +61,7 @@ export const settingsController = {
|
|||||||
auditLogService.logAction('IP_WHITELIST_UPDATED', { updatedKeys });
|
auditLogService.logAction('IP_WHITELIST_UPDATED', { updatedKeys });
|
||||||
} else {
|
} else {
|
||||||
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys });
|
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys });
|
||||||
|
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys }); // 添加通知调用
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.status(200).json({ message: '设置已成功更新' });
|
res.status(200).json({ message: '设置已成功更新' });
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { StatusMonitorService } from './services/status-monitor.service';
|
|||||||
import * as SshService from './services/ssh.service';
|
import * as SshService from './services/ssh.service';
|
||||||
import { DockerService } from './services/docker.service';
|
import { DockerService } from './services/docker.service';
|
||||||
import { AuditLogService } from './services/audit.service';
|
import { AuditLogService } from './services/audit.service';
|
||||||
|
import { NotificationService } from './services/notification.service'; // 添加导入
|
||||||
import { settingsService } from './services/settings.service';
|
import { settingsService } from './services/settings.service';
|
||||||
|
|
||||||
// 扩展 WebSocket 类型以包含会话 ID
|
// 扩展 WebSocket 类型以包含会话 ID
|
||||||
@@ -138,6 +139,7 @@ export const clientStates = new Map<string, ClientState>();
|
|||||||
const sftpService = new SftpService(clientStates);
|
const sftpService = new SftpService(clientStates);
|
||||||
const statusMonitorService = new StatusMonitorService(clientStates);
|
const statusMonitorService = new StatusMonitorService(clientStates);
|
||||||
const auditLogService = new AuditLogService(); // 实例化 AuditLogService
|
const auditLogService = new AuditLogService(); // 实例化 AuditLogService
|
||||||
|
const notificationService = new NotificationService(); // 添加实例
|
||||||
const dockerService = new DockerService(); // 实例化 DockerService (主要用于类型或未来可能的本地调用)
|
const dockerService = new DockerService(); // 实例化 DockerService (主要用于类型或未来可能的本地调用)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -527,6 +529,13 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
sessionId: newSessionId,
|
sessionId: newSessionId,
|
||||||
ip: newState.ipAddress
|
ip: newState.ipAddress
|
||||||
});
|
});
|
||||||
|
notificationService.sendNotification('SSH_CONNECT_SUCCESS', { // 添加通知调用
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
|
connectionId: dbConnectionId,
|
||||||
|
sessionId: newSessionId,
|
||||||
|
ip: newState.ipAddress
|
||||||
|
});
|
||||||
|
|
||||||
// 7. 异步初始化 SFTP 和启动状态监控
|
// 7. 异步初始化 SFTP 和启动状态监控
|
||||||
console.log(`WebSocket: 会话 ${newSessionId} 正在异步初始化 SFTP...`);
|
console.log(`WebSocket: 会话 ${newSessionId} 正在异步初始化 SFTP...`);
|
||||||
@@ -620,6 +629,14 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
ip: newState.ipAddress,
|
ip: newState.ipAddress,
|
||||||
reason: shellError.message
|
reason: shellError.message
|
||||||
});
|
});
|
||||||
|
notificationService.sendNotification('SSH_SHELL_FAILURE', { // 添加通知调用
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
|
connectionId: dbConnectionId,
|
||||||
|
sessionId: newSessionId,
|
||||||
|
ip: newState.ipAddress,
|
||||||
|
reason: shellError.message
|
||||||
|
});
|
||||||
ws.send(JSON.stringify({ type: 'ssh:error', payload: `打开 Shell 失败: ${shellError.message}` }));
|
ws.send(JSON.stringify({ type: 'ssh:error', payload: `打开 Shell 失败: ${shellError.message}` }));
|
||||||
cleanupClientConnection(newSessionId);
|
cleanupClientConnection(newSessionId);
|
||||||
}
|
}
|
||||||
@@ -645,6 +662,13 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
ip: clientIp,
|
ip: clientIp,
|
||||||
reason: connectError.message
|
reason: connectError.message
|
||||||
});
|
});
|
||||||
|
notificationService.sendNotification('SSH_CONNECT_FAILURE', { // 添加通知调用
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
|
connectionId: dbConnectionId,
|
||||||
|
ip: clientIp,
|
||||||
|
reason: connectError.message
|
||||||
|
});
|
||||||
ws.send(JSON.stringify({ type: 'ssh:error', payload: `连接失败: ${connectError.message}` }));
|
ws.send(JSON.stringify({ type: 'ssh:error', payload: `连接失败: ${connectError.message}` }));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user