feat: Add manual event notification endpoint for admins

- POST /api/events/:eventId/notify - send notifications from admin's email
- sendSingleEventNotification() uses admin's primary email account
- getSenderAccountByUserId() to get admin's email credentials

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-12-16 09:06:30 +01:00
parent 2d6198b5f8
commit 548a8effdb
5 changed files with 184 additions and 3 deletions

View File

@@ -222,6 +222,145 @@ const sendNotificationEmail = async (jmapConfig, to, subject, htmlBody, textBody
}
};
/**
* Get sender account by user ID (admin's primary email)
* @param {string} userId - Admin user ID
* @returns {Promise<Object|null>} Sender account with decrypted password
*/
const getSenderAccountByUserId = async (userId) => {
const [result] = await db
.select({
id: emailAccounts.id,
email: emailAccounts.email,
emailPassword: emailAccounts.emailPassword,
jmapAccountId: emailAccounts.jmapAccountId,
})
.from(userEmailAccounts)
.innerJoin(emailAccounts, eq(userEmailAccounts.emailAccountId, emailAccounts.id))
.where(
and(
eq(userEmailAccounts.userId, userId),
eq(userEmailAccounts.isPrimary, true)
)
)
.limit(1);
if (!result) {
logger.error(`Používateľ ${userId} nemá nastavený primárny email účet`);
return null;
}
try {
const decryptedPassword = decryptPassword(result.emailPassword);
return {
id: result.id,
email: result.email,
password: decryptedPassword,
jmapAccountId: result.jmapAccountId,
};
} catch (error) {
logger.error(`Nepodarilo sa dešifrovať heslo pre ${result.email}`, error);
return null;
}
};
/**
* Send notification for a single event to all assigned users
* @param {string} eventId - Event ID
* @param {string} adminUserId - Admin user ID (sender)
* @returns {Promise<{ sent: number, failed: number, skipped: number, eventTitle: string }>}
*/
export const sendSingleEventNotification = async (eventId, adminUserId) => {
logger.info(`=== Odosielam notifikácie pre event ${eventId} od admina ${adminUserId} ===`);
const stats = { sent: 0, failed: 0, skipped: 0, eventTitle: '' };
// Get admin's primary email account
const senderAccount = await getSenderAccountByUserId(adminUserId);
if (!senderAccount) {
throw new Error('Nemáte nastavený primárny email účet. Nastavte ho v profile.');
}
const jmapConfig = {
server: process.env.JMAP_SERVER,
username: senderAccount.email,
password: senderAccount.password,
accountId: senderAccount.jmapAccountId,
};
logger.info(`Odosielam z účtu: ${senderAccount.email}`);
// Get the event with assigned users
const eventData = await db
.select({
eventId: events.id,
title: events.title,
description: events.description,
type: events.type,
start: events.start,
end: events.end,
userId: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(events)
.innerJoin(eventUsers, eq(events.id, eventUsers.eventId))
.innerJoin(users, eq(eventUsers.userId, users.id))
.where(eq(events.id, eventId));
if (eventData.length === 0) {
throw new Error('Event nebol nájdený alebo nemá priradených používateľov');
}
stats.eventTitle = eventData[0].title;
logger.info(`Event: ${stats.eventTitle}, priradených používateľov: ${eventData.length}`);
// Send notifications to each assigned user
for (const row of eventData) {
const { userId, username, firstName } = row;
const event = {
id: row.eventId,
title: row.title,
description: row.description,
type: row.type,
start: row.start,
end: row.end,
};
// Get user's email
const userEmail = await getUserEmail(userId);
if (!userEmail) {
logger.warn(`Používateľ ${username} nemá nastavený primárny email - preskakujem`);
stats.skipped++;
continue;
}
// Generate email content
const subject = generateEventNotificationSubject(event);
const htmlBody = generateEventNotificationHtml({ firstName, username, event });
const textBody = generateEventNotificationText({ firstName, username, event });
// Send email
logger.info(`Odosielam notifikáciu pre ${username} (${userEmail})`);
const success = await sendNotificationEmail(jmapConfig, userEmail, subject, htmlBody, textBody);
if (success) {
logger.success(`Email úspešne odoslaný na ${userEmail}`);
stats.sent++;
} else {
logger.error(`Nepodarilo sa odoslať email na ${userEmail}`);
stats.failed++;
}
}
logger.info(`=== Hotovo: odoslaných ${stats.sent}, neúspešných ${stats.failed}, preskočených ${stats.skipped} ===`);
return stats;
};
/**
* Main function to send event notifications
* @returns {Promise<{ sent: number, failed: number, skipped: number }>}

View File

@@ -1,6 +1,6 @@
import cron from 'node-cron';
import { logger } from '../../utils/logger.js';
import { sendEventNotifications } from './event-notifier.js';
import { sendEventNotifications, sendSingleEventNotification } from './event-notifier.js';
/**
* Parse NOTIFICATION_TIME from env (format: "HH:mm")
@@ -71,3 +71,13 @@ export const triggerEventNotifications = async () => {
logger.info('Manuálne spúšťam kontrolu notifikácií...');
return sendEventNotifications();
};
/**
* Send notification for a single event
* @param {string} eventId - Event ID
* @param {string} adminUserId - Admin user ID (sender)
*/
export const triggerSingleEventNotification = async (eventId, adminUserId) => {
logger.info(`Manuálne spúšťam notifikáciu pre event ${eventId} od admina ${adminUserId}...`);
return sendSingleEventNotification(eventId, adminUserId);
};

View File

@@ -1,5 +1,5 @@
import { logger } from '../utils/logger.js';
import { startCalendarNotificationCron, triggerEventNotifications } from './calendar/index.js';
import { startCalendarNotificationCron, triggerEventNotifications, triggerSingleEventNotification } from './calendar/index.js';
/**
* Start all cron jobs
@@ -14,4 +14,4 @@ export const startAllCronJobs = () => {
};
// Export individual functions for testing/manual triggers
export { triggerEventNotifications };
export { triggerEventNotifications, triggerSingleEventNotification };