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:
@@ -1,4 +1,5 @@
|
|||||||
import * as eventService from '../services/event.service.js';
|
import * as eventService from '../services/event.service.js';
|
||||||
|
import { triggerSingleEventNotification } from '../cron/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get calendar data (events + todos) by month
|
* Get calendar data (events + todos) by month
|
||||||
@@ -106,3 +107,24 @@ export const deleteEvent = async (req, res, next) => {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification for an event to all assigned users (admin only)
|
||||||
|
* POST /api/events/:eventId/notify
|
||||||
|
*/
|
||||||
|
export const sendEventNotification = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { eventId } = req.params;
|
||||||
|
const adminUserId = req.userId;
|
||||||
|
|
||||||
|
const stats = await triggerSingleEventNotification(eventId, adminUserId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
message: `Notifikácie pre "${stats.eventTitle}" odoslané: ${stats.sent}, neúspešné: ${stats.failed}, preskočené: ${stats.skipped}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
* Main function to send event notifications
|
||||||
* @returns {Promise<{ sent: number, failed: number, skipped: number }>}
|
* @returns {Promise<{ sent: number, failed: number, skipped: number }>}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import { logger } from '../../utils/logger.js';
|
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")
|
* 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í...');
|
logger.info('Manuálne spúšťam kontrolu notifikácií...');
|
||||||
return sendEventNotifications();
|
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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { logger } from '../utils/logger.js';
|
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
|
* Start all cron jobs
|
||||||
@@ -14,4 +14,4 @@ export const startAllCronJobs = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Export individual functions for testing/manual triggers
|
// Export individual functions for testing/manual triggers
|
||||||
export { triggerEventNotifications };
|
export { triggerEventNotifications, triggerSingleEventNotification };
|
||||||
|
|||||||
@@ -69,4 +69,14 @@ router.delete(
|
|||||||
eventController.deleteEvent
|
eventController.deleteEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/events/:eventId/notify - Odoslať notifikácie priradeným používateľom (iba admin)
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/:eventId/notify',
|
||||||
|
requireAdmin,
|
||||||
|
validateParams(eventIdSchema),
|
||||||
|
eventController.sendEventNotification
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user