Compare commits
No commits in common. "fd636c6c82514912a3fa92c1e7f0e29a7d4f081f" and "4c5b645487ed6d76e38279a34f1fac90126dc27a" have entirely different histories.
fd636c6c82
...
4c5b645487
@ -3,7 +3,6 @@ import { generateId } from '../../utils.js';
|
|||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const partSkuUsageSchema = new Schema({
|
const partSkuUsageSchema = new Schema({
|
||||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
|
||||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||||
quantity: { type: Number, required: true },
|
quantity: { type: Number, required: true },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -55,7 +55,6 @@ import {
|
|||||||
userNotifierRoutes,
|
userNotifierRoutes,
|
||||||
notificationRoutes,
|
notificationRoutes,
|
||||||
odataRoutes,
|
odataRoutes,
|
||||||
rssRoutes,
|
|
||||||
excelRoutes,
|
excelRoutes,
|
||||||
csvRoutes,
|
csvRoutes,
|
||||||
appLaunchRoutes,
|
appLaunchRoutes,
|
||||||
@ -181,7 +180,6 @@ app.use('/notes', noteRoutes);
|
|||||||
app.use('/usernotifiers', userNotifierRoutes);
|
app.use('/usernotifiers', userNotifierRoutes);
|
||||||
app.use('/notifications', notificationRoutes);
|
app.use('/notifications', notificationRoutes);
|
||||||
app.use('/odata', odataRoutes);
|
app.use('/odata', odataRoutes);
|
||||||
app.use('/rss', rssRoutes);
|
|
||||||
app.use('/excel', excelRoutes);
|
app.use('/excel', excelRoutes);
|
||||||
app.use('/csv', csvRoutes);
|
app.use('/csv', csvRoutes);
|
||||||
app.use('/applaunch', appLaunchRoutes);
|
app.use('/applaunch', appLaunchRoutes);
|
||||||
|
|||||||
@ -53,7 +53,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) Bearer token (email-render JWT for Puppeteer),
|
* Supports: 1) Bearer token (Redis session), 2) Bearer token (email-render JWT for Puppeteer),
|
||||||
* 3) x-host-id + x-auth-code (host auth)
|
* 3) HTTP Basic Auth (username + app password), 4) 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;
|
||||||
@ -92,30 +92,9 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const authenticateWithAppPassword = async (username, secret) => {
|
|
||||||
if (!username || !secret) return null;
|
|
||||||
|
|
||||||
const user = await userModel.findOne({ username });
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const appPasswords = await appPasswordModel
|
|
||||||
.find({ user: user._id, active: true })
|
|
||||||
.select('+secret')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const appPassword of appPasswords) {
|
|
||||||
const storedHash = appPassword.secret;
|
|
||||||
if (storedHash && (await bcrypt.compare(secret, storedHash))) {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAppAuthenticated = async (req, res, next) => {
|
const isAppAuthenticated = async (req, res, next) => {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
// Supports HTTP Basic Auth (username + app password secret)
|
// Try HTTP Basic Auth (username + app password secret)
|
||||||
if (authHeader?.startsWith('Basic ')) {
|
if (authHeader?.startsWith('Basic ')) {
|
||||||
try {
|
try {
|
||||||
logger.debug('Basic auth header present');
|
logger.debug('Basic auth header present');
|
||||||
@ -124,12 +103,23 @@ const isAppAuthenticated = async (req, res, next) => {
|
|||||||
const colonIndex = credentials.indexOf(':');
|
const colonIndex = credentials.indexOf(':');
|
||||||
const username = credentials.substring(0, colonIndex).trim();
|
const username = credentials.substring(0, colonIndex).trim();
|
||||||
const secret = credentials.substring(colonIndex + 1).trim();
|
const secret = credentials.substring(colonIndex + 1).trim();
|
||||||
const user = await authenticateWithAppPassword(username, secret);
|
if (username && secret) {
|
||||||
|
const user = await userModel.findOne({ username });
|
||||||
if (user) {
|
if (user) {
|
||||||
|
const appPasswords = await appPasswordModel
|
||||||
|
.find({ user: user._id, active: true })
|
||||||
|
.select('+secret')
|
||||||
|
.lean();
|
||||||
|
for (const appPassword of appPasswords) {
|
||||||
|
const storedHash = appPassword.secret;
|
||||||
|
if (storedHash && (await bcrypt.compare(secret, storedHash))) {
|
||||||
req.user = user;
|
req.user = user;
|
||||||
req.session = { user };
|
req.session = { user };
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Basic auth error:', error.message);
|
logger.error('Basic auth error:', error.message);
|
||||||
}
|
}
|
||||||
@ -137,22 +127,6 @@ const isAppAuthenticated = async (req, res, next) => {
|
|||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAppQueryAuthenticated = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const username = typeof req.query.u === 'string' ? req.query.u.trim() : '';
|
|
||||||
const secret = typeof req.query.p === 'string' ? req.query.p : '';
|
|
||||||
const user = await authenticateWithAppPassword(username, secret);
|
|
||||||
if (user) {
|
|
||||||
req.user = user;
|
|
||||||
req.session = { user };
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Query auth error:', error.message);
|
|
||||||
}
|
|
||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearUserCache = () => {
|
const clearUserCache = () => {
|
||||||
userCache.flushAll();
|
userCache.flushAll();
|
||||||
logger.info('User cache cleared');
|
logger.info('User cache cleared');
|
||||||
@ -170,7 +144,6 @@ const removeUserFromCache = (username) => {
|
|||||||
export {
|
export {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAppAuthenticated,
|
isAppAuthenticated,
|
||||||
isAppQueryAuthenticated,
|
|
||||||
lookupUser,
|
lookupUser,
|
||||||
clearUserCache,
|
clearUserCache,
|
||||||
getUserCacheStats,
|
getUserCacheStats,
|
||||||
|
|||||||
@ -48,7 +48,6 @@ import noteRoutes from './misc/notes.js';
|
|||||||
import userNotifierRoutes from './misc/usernotifiers.js';
|
import userNotifierRoutes from './misc/usernotifiers.js';
|
||||||
import notificationRoutes from './misc/notifications.js';
|
import notificationRoutes from './misc/notifications.js';
|
||||||
import odataRoutes from './misc/odata.js';
|
import odataRoutes from './misc/odata.js';
|
||||||
import rssRoutes from './misc/rss.js';
|
|
||||||
import excelRoutes from './misc/excel.js';
|
import excelRoutes from './misc/excel.js';
|
||||||
import csvRoutes from './misc/csv.js';
|
import csvRoutes from './misc/csv.js';
|
||||||
import appLaunchRoutes from './misc/applaunch.js';
|
import appLaunchRoutes from './misc/applaunch.js';
|
||||||
@ -104,7 +103,6 @@ export {
|
|||||||
userNotifierRoutes,
|
userNotifierRoutes,
|
||||||
notificationRoutes,
|
notificationRoutes,
|
||||||
odataRoutes,
|
odataRoutes,
|
||||||
rssRoutes,
|
|
||||||
excelRoutes,
|
excelRoutes,
|
||||||
csvRoutes,
|
csvRoutes,
|
||||||
appLaunchRoutes,
|
appLaunchRoutes,
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAppQueryAuthenticated } from '../../keycloak.js';
|
|
||||||
import { listRssRouteHandler } from '../../services/misc/rss.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/:objectType', isAppQueryAuthenticated, listRssRouteHandler);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -35,7 +35,7 @@ export const listProductSkusRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['priceTaxRate', 'costTaxRate', 'product'],
|
populate: ['priceTaxRate', 'costTaxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -83,8 +83,6 @@ export const getProductSkuRouteHandler = async (req, res) => {
|
|||||||
'priceTaxRate',
|
'priceTaxRate',
|
||||||
'costTaxRate',
|
'costTaxRate',
|
||||||
'parts.partSku',
|
'parts.partSku',
|
||||||
'parts.part',
|
|
||||||
'product',
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
import config from '../../config.js';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import { getModelByName } from './model.js';
|
|
||||||
import { listObjectsOData } from '../../database/odata.js';
|
|
||||||
import { getFilter } from '../../utils.js';
|
|
||||||
import { getModelFilterFields, parseOrderBy } from './export.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('RSS');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
function escapeXml(str) {
|
|
||||||
return String(str)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemTitle(objectType, row) {
|
|
||||||
const preferredTitle = row.name || row.title || row.label || row.code;
|
|
||||||
if (preferredTitle) return String(preferredTitle);
|
|
||||||
return `${objectType} ${row._id || 'entry'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemDescription(row) {
|
|
||||||
const ignoredKeys = new Set(['_id', 'createdAt', 'updatedAt']);
|
|
||||||
const summary = Object.entries(row || {})
|
|
||||||
.filter(
|
|
||||||
([key, val]) => !ignoredKeys.has(key) && val !== null && val !== undefined && val !== ''
|
|
||||||
)
|
|
||||||
.slice(0, 10)
|
|
||||||
.map(([key, val]) => `${key}: ${typeof val === 'object' ? JSON.stringify(val) : String(val)}`)
|
|
||||||
.join(' | ');
|
|
||||||
|
|
||||||
if (summary) return summary;
|
|
||||||
return JSON.stringify(row || {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRssXml({ objectType, feedUrl, rows }) {
|
|
||||||
const now = new Date().toUTCString();
|
|
||||||
const itemsXml = rows
|
|
||||||
.map((row) => {
|
|
||||||
const rowId = row?._id ? String(row._id) : `${objectType}-${Date.now()}`;
|
|
||||||
const pubDateSource = row?.updatedAt || row?.createdAt;
|
|
||||||
const pubDate = pubDateSource ? new Date(pubDateSource).toUTCString() : now;
|
|
||||||
const itemLink = `${feedUrl}#${encodeURIComponent(rowId)}`;
|
|
||||||
return [
|
|
||||||
'<item>',
|
|
||||||
` <title>${escapeXml(getItemTitle(objectType, row || {}))}</title>`,
|
|
||||||
` <link>${escapeXml(itemLink)}</link>`,
|
|
||||||
` <guid isPermaLink="false">${escapeXml(`${objectType}:${rowId}`)}</guid>`,
|
|
||||||
` <description>${escapeXml(getItemDescription(row || {}))}</description>`,
|
|
||||||
` <pubDate>${escapeXml(pubDate)}</pubDate>`,
|
|
||||||
'</item>',
|
|
||||||
].join('\n');
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<rss version="2.0">
|
|
||||||
<channel>
|
|
||||||
<title>${escapeXml(`FarmControl ${objectType} RSS Feed`)}</title>
|
|
||||||
<link>${escapeXml(feedUrl)}</link>
|
|
||||||
<description>${escapeXml(`Recent ${objectType} updates from FarmControl`)}</description>
|
|
||||||
<lastBuildDate>${escapeXml(now)}</lastBuildDate>
|
|
||||||
${itemsXml}
|
|
||||||
</channel>
|
|
||||||
</rss>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route handler for GET /rss/:objectType
|
|
||||||
* Supports OData-style query options: $top, $skip, $orderby and allowed filters.
|
|
||||||
*/
|
|
||||||
export const listRssRouteHandler = async (req, res) => {
|
|
||||||
const objectType = req.params.objectType;
|
|
||||||
const entry = getModelByName(objectType);
|
|
||||||
|
|
||||||
if (!entry?.model) {
|
|
||||||
logger.warn(`RSS: unknown object type "${objectType}"`);
|
|
||||||
return res.status(404).send({ error: `Unknown object type: ${objectType}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { $top, $skip, $orderby } = req.query;
|
|
||||||
const queryWithoutAuth = { ...req.query };
|
|
||||||
delete queryWithoutAuth.u;
|
|
||||||
delete queryWithoutAuth.p;
|
|
||||||
const limit = $top ? Math.min(parseInt($top, 10) || 25, 1000) : 25;
|
|
||||||
const skip = $skip ? Math.max(0, parseInt($skip, 10) || 0) : 0;
|
|
||||||
const page = Math.floor(skip / limit) + 1;
|
|
||||||
const { sort, order } = parseOrderBy($orderby);
|
|
||||||
const allowedFilters = getModelFilterFields(objectType);
|
|
||||||
const filter = getFilter(queryWithoutAuth, allowedFilters);
|
|
||||||
|
|
||||||
const result = await listObjectsOData({
|
|
||||||
model: entry.model,
|
|
||||||
populate: [],
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
filter,
|
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
pagination: true,
|
|
||||||
project: '-__v',
|
|
||||||
count: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('RSS list error:', result.error);
|
|
||||||
return res.status(result.code || 500).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = config.app?.urlApi || '';
|
|
||||||
const feedUrl = `${baseUrl}/rss/${objectType}`;
|
|
||||||
const rows = result?.value || [];
|
|
||||||
const xml = buildRssXml({ objectType, feedUrl, rows });
|
|
||||||
|
|
||||||
res.set('Content-Type', 'application/rss+xml; charset=utf-8');
|
|
||||||
res.send(xml);
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user