192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
// server/src/services/notifications.ts
|
|
import { createTransport } from 'nodemailer';
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { randomUUID } from 'crypto';
|
|
import type { MonitorUser, Notification, UserRole } from '../types/index.js';
|
|
|
|
// ─── Persistent JSON storage (simple file-based) ───
|
|
const DATA_DIR = resolve(process.cwd(), 'data');
|
|
const USERS_FILE = resolve(DATA_DIR, 'users.json');
|
|
const NOTIFICATIONS_FILE = resolve(DATA_DIR, 'notifications.json');
|
|
|
|
function ensureDataDir() {
|
|
try { mkdirSync(DATA_DIR, { recursive: true }); } catch {}
|
|
}
|
|
|
|
function loadUsers(): MonitorUser[] {
|
|
ensureDataDir();
|
|
if (!existsSync(USERS_FILE)) {
|
|
// Seed with the existing config users
|
|
const seed: MonitorUser[] = [
|
|
{ id: randomUUID(), email: 'nafih@bshtechnologies.in', name: 'Nafih', role: 'server-admin', notifyOnError: true, createdAt: new Date().toISOString() },
|
|
{ id: randomUUID(), email: 'vivek@bshtechnologies.in', name: 'Vivek', role: 'developer', notifyOnError: true, createdAt: new Date().toISOString() },
|
|
];
|
|
writeFileSync(USERS_FILE, JSON.stringify(seed, null, 2));
|
|
return seed;
|
|
}
|
|
return JSON.parse(readFileSync(USERS_FILE, 'utf-8'));
|
|
}
|
|
|
|
function saveUsers(users: MonitorUser[]) {
|
|
ensureDataDir();
|
|
writeFileSync(USERS_FILE, JSON.stringify(users, null, 2));
|
|
}
|
|
|
|
function loadNotifications(): Notification[] {
|
|
ensureDataDir();
|
|
if (!existsSync(NOTIFICATIONS_FILE)) {
|
|
writeFileSync(NOTIFICATIONS_FILE, '[]');
|
|
return [];
|
|
}
|
|
return JSON.parse(readFileSync(NOTIFICATIONS_FILE, 'utf-8'));
|
|
}
|
|
|
|
function saveNotifications(notifs: Notification[]) {
|
|
ensureDataDir();
|
|
// Keep only last 200 notifications
|
|
const trimmed = notifs.slice(-200);
|
|
writeFileSync(NOTIFICATIONS_FILE, JSON.stringify(trimmed, null, 2));
|
|
}
|
|
|
|
// ─── SMTP Transport ───
|
|
const smtpTransport = createTransport({
|
|
host: process.env.SMTP_HOST ?? 'mail.bshtech.net',
|
|
port: parseInt(process.env.SMTP_PORT ?? '587'),
|
|
secure: false,
|
|
auth: {
|
|
user: process.env.SMTP_USER ?? 'noreply@bshtech.net',
|
|
pass: process.env.SMTP_PASS ?? 'Ev3ntifyN0Reply2026',
|
|
},
|
|
tls: { rejectUnauthorized: false },
|
|
});
|
|
|
|
// ─── Public API ───
|
|
|
|
export function getUsers(): MonitorUser[] {
|
|
return loadUsers();
|
|
}
|
|
|
|
export function addUser(email: string, name: string, role: UserRole, notifyOnError = true): MonitorUser {
|
|
const users = loadUsers();
|
|
if (users.find(u => u.email === email)) {
|
|
throw new Error(`User with email ${email} already exists`);
|
|
}
|
|
const user: MonitorUser = {
|
|
id: randomUUID(),
|
|
email,
|
|
name,
|
|
role,
|
|
notifyOnError,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
users.push(user);
|
|
saveUsers(users);
|
|
return user;
|
|
}
|
|
|
|
export function updateUser(id: string, updates: Partial<Pick<MonitorUser, 'name' | 'role' | 'notifyOnError'>>): MonitorUser {
|
|
const users = loadUsers();
|
|
const idx = users.findIndex(u => u.id === id);
|
|
if (idx < 0) throw new Error('User not found');
|
|
users[idx] = { ...users[idx], ...updates };
|
|
saveUsers(users);
|
|
return users[idx];
|
|
}
|
|
|
|
export function deleteUser(id: string): void {
|
|
const users = loadUsers();
|
|
const filtered = users.filter(u => u.id !== id);
|
|
if (filtered.length === users.length) throw new Error('User not found');
|
|
saveUsers(filtered);
|
|
}
|
|
|
|
export function getNotifications(limit = 50): Notification[] {
|
|
return loadNotifications().reverse().slice(0, limit);
|
|
}
|
|
|
|
export function markNotificationRead(id: string): void {
|
|
const notifs = loadNotifications();
|
|
const n = notifs.find(n => n.id === id);
|
|
if (n) {
|
|
n.read = true;
|
|
saveNotifications(notifs);
|
|
}
|
|
}
|
|
|
|
export function markAllRead(): void {
|
|
const notifs = loadNotifications();
|
|
notifs.forEach(n => n.read = true);
|
|
saveNotifications(notifs);
|
|
}
|
|
|
|
export function getUnreadCount(): number {
|
|
return loadNotifications().filter(n => !n.read).length;
|
|
}
|
|
|
|
export async function createNotification(
|
|
type: Notification['type'],
|
|
title: string,
|
|
message: string,
|
|
source: string,
|
|
): Promise<Notification> {
|
|
const notif: Notification = {
|
|
id: randomUUID(),
|
|
type,
|
|
title,
|
|
message,
|
|
source,
|
|
timestamp: new Date().toISOString(),
|
|
read: false,
|
|
emailSent: false,
|
|
};
|
|
|
|
// Save to file
|
|
const notifs = loadNotifications();
|
|
notifs.push(notif);
|
|
saveNotifications(notifs);
|
|
|
|
// Send email to subscribed users
|
|
if (type === 'error' || type === 'warning') {
|
|
const users = loadUsers().filter(u => u.notifyOnError);
|
|
if (users.length > 0) {
|
|
try {
|
|
await smtpTransport.sendMail({
|
|
from: '"Eventify Monitor" <noreply@bshtech.net>',
|
|
to: users.map(u => u.email).join(', '),
|
|
subject: `[${type.toUpperCase()}] ${title}`,
|
|
html: `
|
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<div style="background: ${type === 'error' ? '#dc2626' : '#f59e0b'}; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="margin: 0; font-size: 18px;">${type === 'error' ? 'Error Alert' : 'Warning'}: ${title}</h2>
|
|
</div>
|
|
<div style="background: #f9fafb; padding: 24px; border: 1px solid #e5e7eb; border-top: none; border-radius: 0 0 8px 8px;">
|
|
<p style="margin: 0 0 12px; color: #374151;">${message}</p>
|
|
<p style="margin: 0 0 8px; color: #6b7280; font-size: 13px;"><strong>Source:</strong> ${source}</p>
|
|
<p style="margin: 0; color: #6b7280; font-size: 13px;"><strong>Time:</strong> ${new Date().toLocaleString()}</p>
|
|
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 16px 0;">
|
|
<p style="margin: 0; color: #9ca3af; font-size: 12px;">
|
|
<a href="https://status.eventifyplus.com" style="color: #2563eb;">View Dashboard</a> | Eventify Server Monitor
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
notif.emailSent = true;
|
|
// Update the saved notification
|
|
const updated = loadNotifications();
|
|
const idx = updated.findIndex(n => n.id === notif.id);
|
|
if (idx >= 0) {
|
|
updated[idx].emailSent = true;
|
|
saveNotifications(updated);
|
|
}
|
|
console.log(`[Notifications] Email sent to ${users.length} users for: ${title}`);
|
|
} catch (err) {
|
|
console.error(`[Notifications] Failed to send email:`, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
return notif;
|
|
}
|