farmcontrol-api/src/keycloak.js
Tom Butcher a9c4b29f9f
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Improved notifications.
2026-03-01 19:21:05 +00:00

114 lines
3.3 KiB
JavaScript

/**
* Authentication middleware - uses Redis session store.
* Keycloak is used only for login/refresh; session validation is done via Redis.
*/
import config, { getEnvironment } from './config.js';
import log4js from 'log4js';
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, lookupUserByToken } from './services/misc/auth.js';
const logger = log4js.getLogger('Keycloak');
logger.level = config.server.logLevel || 'info';
const userCache = new NodeCache({ stdTTL: 300 });
userCache.on('expired', (key, value) => {
logger.debug(`Cache entry expired: ${key}`);
});
userCache.on('flush', () => {
logger.info('Cache flushed');
});
const lookupUser = async (preferredUsername) => {
try {
const cachedUser = userCache.get(preferredUsername);
if (cachedUser) {
logger.debug(`User found in cache: ${preferredUsername}`);
return cachedUser;
}
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
const user = await userModel.findOne({ username: preferredUsername });
if (user) {
userCache.set(preferredUsername, user);
logger.debug(`User stored in cache: ${preferredUsername}`);
return user;
}
logger.warn(`User not found in database: ${preferredUsername}`);
return null;
} catch (error) {
logger.error(`Error looking up user ${preferredUsername}:`, error.message);
return null;
}
};
/**
* Middleware to check if the user is authenticated.
* 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;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
const session = await getSession(token);
if (session && session.expiresAt > Date.now()) {
req.user = session.user;
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);
}
}
const hostId = req.headers['x-host-id'];
const authCode = req.headers['x-auth-code'];
if (hostId && authCode) {
const host = await getObject({ model: hostModel, id: hostId });
if (host && host.authCode === authCode) {
return next();
}
}
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
};
const clearUserCache = () => {
userCache.flushAll();
logger.info('User cache cleared');
};
const getUserCacheStats = () => {
return userCache.getStats();
};
const removeUserFromCache = (username) => {
userCache.del(username);
logger.debug(`User removed from cache: ${username}`);
};
export {
isAuthenticated,
lookupUser,
clearUserCache,
getUserCacheStats,
removeUserFromCache,
getEnvironment,
};