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 { userModel } from './database/schemas/management/user.schema.js';
|
||||||
import { getObject } from './database/database.js';
|
import { getObject } from './database/database.js';
|
||||||
import { hostModel } from './database/schemas/management/host.schema.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');
|
const logger = log4js.getLogger('Keycloak');
|
||||||
logger.level = config.server.logLevel || 'info';
|
logger.level = config.server.logLevel || 'info';
|
||||||
@ -50,7 +50,7 @@ const lookupUser = async (preferredUsername) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to check if the user is authenticated.
|
* 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 isAuthenticated = async (req, res, next) => {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
@ -64,6 +64,14 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
req.session = session;
|
req.session = session;
|
||||||
return next();
|
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) {
|
} catch (error) {
|
||||||
logger.error('Session lookup error:', error.message);
|
logger.error('Session lookup error:', error.message);
|
||||||
}
|
}
|
||||||
@ -77,7 +85,7 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
|
||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -41,8 +41,18 @@ const logger = log4js.getLogger('MailWorker');
|
|||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
async function sendEmail(payload) {
|
async function sendEmail(payload) {
|
||||||
const { email, title, message, type, metadata, smtpConfig, urlClient, createdAt, updatedAt, authCode } =
|
const {
|
||||||
payload;
|
email,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
metadata,
|
||||||
|
smtpConfig,
|
||||||
|
urlClient,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
authCode,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
if (!email || !smtpConfig?.host) {
|
if (!email || !smtpConfig?.host) {
|
||||||
logger.warn('Missing email or SMTP config, skipping...');
|
logger.warn('Missing email or SMTP config, skipping...');
|
||||||
@ -56,11 +66,9 @@ async function sendEmail(payload) {
|
|||||||
email: email || '',
|
email: email || '',
|
||||||
createdAt: createdAt || new Date(),
|
createdAt: createdAt || new Date(),
|
||||||
updatedAt: updatedAt || new Date(),
|
updatedAt: updatedAt || new Date(),
|
||||||
|
authCode: authCode || '',
|
||||||
metadata: JSON.stringify(metadata || {}),
|
metadata: JSON.stringify(metadata || {}),
|
||||||
});
|
});
|
||||||
if (authCode) {
|
|
||||||
params.set('authCode', authCode);
|
|
||||||
}
|
|
||||||
const templateUrl = `${baseUrl(urlClient)}/email/notification?${params.toString()}`;
|
const templateUrl = `${baseUrl(urlClient)}/email/notification?${params.toString()}`;
|
||||||
|
|
||||||
logger.debug('Rendering template...');
|
logger.debug('Rendering template...');
|
||||||
@ -70,14 +78,24 @@ async function sendEmail(payload) {
|
|||||||
let browser;
|
let browser;
|
||||||
try {
|
try {
|
||||||
browser = await puppeteer.launch({
|
browser = await puppeteer.launch({
|
||||||
headless: true,
|
headless: 'new',
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-features=SameSiteByDefaultCookies',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const page = await browser.newPage();
|
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 });
|
await page.waitForSelector('#email-notification-root[data-rendered="true"]', { timeout: 5000 });
|
||||||
// Wait for Ant Design CSS-in-JS to finish injecting styles
|
// 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(() => {
|
html = await page.evaluate(() => {
|
||||||
const root = document.getElementById('email-notification-root');
|
const root = document.getElementById('email-notification-root');
|
||||||
if (!root) return document.documentElement.outerHTML;
|
if (!root) return document.documentElement.outerHTML;
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const lookupUserByEmailRenderToken = async (token) => {
|
|||||||
return cachedUser;
|
return cachedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.decode(token);
|
const decodedToken = jwt.verify(token, config.auth.sessionSecret);
|
||||||
if (!decodedToken || !decodedToken.preferred_username) {
|
if (!decodedToken || !decodedToken.preferred_username) {
|
||||||
logger.trace('Invalid token or missing preferred_username');
|
logger.trace('Invalid token or missing preferred_username');
|
||||||
return null;
|
return null;
|
||||||
@ -325,12 +325,14 @@ export const logoutRouteHandler = async (req, res) => {
|
|||||||
export const validateTokenMiddleware = async (req, res, next) => {
|
export const validateTokenMiddleware = async (req, res, next) => {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
logger.debug('No auth header or not bearer token');
|
||||||
return res.status(401).json({ error: 'Not authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not authenticated', code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
const session = await getSession(token);
|
const session = await getSession(token);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
logger.debug('Session not found');
|
||||||
return res.status(401).json({ error: 'Session invalid or expired', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Session invalid or expired', code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
getModelHistory,
|
getModelHistory,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
import { newNoteNotification } from '../../utils.js';
|
||||||
|
|
||||||
const logger = log4js.getLogger('Notes');
|
const logger = log4js.getLogger('Notes');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -127,6 +128,9 @@ export const newNoteRouteHandler = async (req, res) => {
|
|||||||
newData,
|
newData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await newNoteNotification({ ...result, ...newData }, req.user);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
logger.error('No note created:', result.error);
|
logger.error('No note created:', result.error);
|
||||||
return res.status(result.code).send(result);
|
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,
|
old: changedOldValues,
|
||||||
new: changedNewValues,
|
new: changedNewValues,
|
||||||
objectType: parentType,
|
objectType: parentType,
|
||||||
object: { _id: parentId },
|
object: { _id: String(parentId ?? '') },
|
||||||
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
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) {
|
async function deleteNotification(object, parentId, parentType, user) {
|
||||||
const model = getModelByName(parentType);
|
const model = getModelByName(parentType);
|
||||||
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
const objectName = object?.name || model?.label || parentType;
|
||||||
await notfiyObjectUserNotifiers(
|
await notfiyObjectUserNotifiers(
|
||||||
parentId,
|
parentId,
|
||||||
parentType,
|
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) {
|
async function getAuditLogs(idOrIds) {
|
||||||
if (Array.isArray(idOrIds)) {
|
if (Array.isArray(idOrIds)) {
|
||||||
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
||||||
@ -875,6 +901,7 @@ export {
|
|||||||
getAuditLogs,
|
getAuditLogs,
|
||||||
flatternObjectIds,
|
flatternObjectIds,
|
||||||
expandObjectIds,
|
expandObjectIds,
|
||||||
|
newNoteNotification,
|
||||||
distributeUpdate,
|
distributeUpdate,
|
||||||
distributeStats,
|
distributeStats,
|
||||||
distributeNew,
|
distributeNew,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user