Improved notifications.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit is contained in:
parent
6e3b900423
commit
a9c4b29f9f
@ -8,7 +8,7 @@ import NodeCache from 'node-cache';
|
||||
import { userModel } from './database/schemas/management/user.schema.js';
|
||||
import { getObject } from './database/database.js';
|
||||
import { hostModel } from './database/schemas/management/host.schema.js';
|
||||
import { getSession } from './services/misc/auth.js';
|
||||
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
||||
|
||||
const logger = log4js.getLogger('Keycloak');
|
||||
logger.level = config.server.logLevel || 'info';
|
||||
@ -50,7 +50,7 @@ const lookupUser = async (preferredUsername) => {
|
||||
|
||||
/**
|
||||
* Middleware to check if the user is authenticated.
|
||||
* Supports: 1) Bearer token (Redis session), 2) x-host-id + x-auth-code (host auth)
|
||||
* Supports: 1) Bearer token (Redis session), 2) Bearer token (email-render JWT for Puppeteer), 3) x-host-id + x-auth-code (host auth)
|
||||
*/
|
||||
const isAuthenticated = async (req, res, next) => {
|
||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||
@ -64,6 +64,14 @@ const isAuthenticated = async (req, res, next) => {
|
||||
req.session = session;
|
||||
return next();
|
||||
}
|
||||
|
||||
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
||||
const user = await lookupUserByToken(token);
|
||||
if (user) {
|
||||
req.user = user;
|
||||
req.session = { user };
|
||||
return next();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Session lookup error:', error.message);
|
||||
}
|
||||
@ -77,7 +85,7 @@ const isAuthenticated = async (req, res, next) => {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
|
||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||
};
|
||||
|
||||
|
||||
@ -41,8 +41,18 @@ const logger = log4js.getLogger('MailWorker');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
async function sendEmail(payload) {
|
||||
const { email, title, message, type, metadata, smtpConfig, urlClient, createdAt, updatedAt, authCode } =
|
||||
payload;
|
||||
const {
|
||||
email,
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
metadata,
|
||||
smtpConfig,
|
||||
urlClient,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
authCode,
|
||||
} = payload;
|
||||
|
||||
if (!email || !smtpConfig?.host) {
|
||||
logger.warn('Missing email or SMTP config, skipping...');
|
||||
@ -56,11 +66,9 @@ async function sendEmail(payload) {
|
||||
email: email || '',
|
||||
createdAt: createdAt || new Date(),
|
||||
updatedAt: updatedAt || new Date(),
|
||||
authCode: authCode || '',
|
||||
metadata: JSON.stringify(metadata || {}),
|
||||
});
|
||||
if (authCode) {
|
||||
params.set('authCode', authCode);
|
||||
}
|
||||
const templateUrl = `${baseUrl(urlClient)}/email/notification?${params.toString()}`;
|
||||
|
||||
logger.debug('Rendering template...');
|
||||
@ -70,14 +78,24 @@ async function sendEmail(payload) {
|
||||
let browser;
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-features=SameSiteByDefaultCookies',
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.goto(templateUrl, { waitUntil: 'networkidle0', timeout: 15000 });
|
||||
page.on('console', (msg) => {
|
||||
const text = msg.text();
|
||||
const type = msg.type();
|
||||
logger.trace(`Puppeteer [${type}]: ${text}`);
|
||||
});
|
||||
await page.goto(templateUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
await page.waitForSelector('#email-notification-root[data-rendered="true"]', { timeout: 5000 });
|
||||
// Wait for Ant Design CSS-in-JS to finish injecting styles
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
logger.debug('Waiting for 1.5 seconds for page to render...');
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
html = await page.evaluate(() => {
|
||||
const root = document.getElementById('email-notification-root');
|
||||
if (!root) return document.documentElement.outerHTML;
|
||||
|
||||
@ -39,7 +39,7 @@ const lookupUserByEmailRenderToken = async (token) => {
|
||||
return cachedUser;
|
||||
}
|
||||
|
||||
const decodedToken = jwt.decode(token);
|
||||
const decodedToken = jwt.verify(token, config.auth.sessionSecret);
|
||||
if (!decodedToken || !decodedToken.preferred_username) {
|
||||
logger.trace('Invalid token or missing preferred_username');
|
||||
return null;
|
||||
@ -325,12 +325,14 @@ export const logoutRouteHandler = async (req, res) => {
|
||||
export const validateTokenMiddleware = async (req, res, next) => {
|
||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
logger.debug('No auth header or not bearer token');
|
||||
return res.status(401).json({ error: 'Not authenticated', code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const session = await getSession(token);
|
||||
if (!session) {
|
||||
logger.debug('Session not found');
|
||||
return res.status(401).json({ error: 'Session invalid or expired', code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
getModelHistory,
|
||||
} from '../../database/database.js';
|
||||
import mongoose from 'mongoose';
|
||||
import { newNoteNotification } from '../../utils.js';
|
||||
|
||||
const logger = log4js.getLogger('Notes');
|
||||
logger.level = config.server.logLevel;
|
||||
@ -127,6 +128,9 @@ export const newNoteRouteHandler = async (req, res) => {
|
||||
newData,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
await newNoteNotification({ ...result, ...newData }, req.user);
|
||||
|
||||
if (result.error) {
|
||||
logger.error('No note created:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
|
||||
33
src/utils.js
33
src/utils.js
@ -434,8 +434,12 @@ async function editNotification(oldValue, newValue, parentId, parentType, user)
|
||||
old: changedOldValues,
|
||||
new: changedNewValues,
|
||||
objectType: parentType,
|
||||
object: { _id: parentId },
|
||||
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||
object: { _id: String(parentId ?? '') },
|
||||
user: {
|
||||
_id: String(user?._id ?? ''),
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -461,7 +465,7 @@ async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
||||
|
||||
async function deleteNotification(object, parentId, parentType, user) {
|
||||
const model = getModelByName(parentType);
|
||||
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||
const objectName = object?.name || model?.label || parentType;
|
||||
await notfiyObjectUserNotifiers(
|
||||
parentId,
|
||||
parentType,
|
||||
@ -477,6 +481,28 @@ async function deleteNotification(object, parentId, parentType, user) {
|
||||
);
|
||||
}
|
||||
|
||||
async function newNoteNotification(note, user) {
|
||||
const model = getModelByName(note.parentType);
|
||||
const objectName = model?.label ?? note.parentType;
|
||||
await notfiyObjectUserNotifiers(
|
||||
note.parent,
|
||||
note.parentType,
|
||||
`New note added to ${objectName.toLowerCase()} by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`,
|
||||
`A new note has been created.`,
|
||||
'newNote',
|
||||
{
|
||||
objectType: note.parentType,
|
||||
object: { _id: String(note.parent ?? '') },
|
||||
note: note,
|
||||
user: {
|
||||
_id: String(user?._id ?? ''),
|
||||
firstName: user?.firstName,
|
||||
lastName: user?.lastName,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getAuditLogs(idOrIds) {
|
||||
if (Array.isArray(idOrIds)) {
|
||||
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
||||
@ -875,6 +901,7 @@ export {
|
||||
getAuditLogs,
|
||||
flatternObjectIds,
|
||||
expandObjectIds,
|
||||
newNoteNotification,
|
||||
distributeUpdate,
|
||||
distributeStats,
|
||||
distributeNew,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user