farmcontrol-api/src/keycloak.js
Tom Butcher ff60d6cf09 Enhance document size and template management with padding options
This commit introduces new padding options for document sizes, allowing for customizable print padding and specific padding values (left, right, top, bottom). The document size schema has been updated to include these new fields, and corresponding route handlers have been modified to handle padding data during document size creation and editing. Additionally, the template manager has been updated to apply padding when rendering templates, ensuring that the layout respects the specified padding values. Tests have been added to verify the correct application of padding in both document sizes and templates, improving the overall document formatting capabilities.
2026-08-21 22:39:13 +01:00

202 lines
6.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 bcrypt from 'bcrypt';
import { userModel } from './database/schemas/management/user.schema.js';
import { appPasswordModel } from './database/schemas/management/apppassword.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, _objectType: '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, _objectType: '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) {
req.user = { ...host, _objectType: 'host' };
return next();
}
}
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
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 }).lean();
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 authHeader = req.headers.authorization || req.headers.Authorization;
const apiKey = req.headers['x-api-key'];
const userParam = req.params.username;
const passwordParam = req.params.password;
logger.debug('App authentication request', {
hasBasicAuth: authHeader?.startsWith('Basic ') === true,
hasApiKey: Boolean(apiKey),
hasPasswordParam: Boolean(passwordParam),
userParam,
});
// Supports HTTP Basic Auth (username + app password secret)
if (authHeader?.startsWith('Basic ')) {
try {
logger.debug('Basic auth header present');
const base64Credentials = authHeader.substring(6);
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
const colonIndex = credentials.indexOf(':');
const username = credentials.substring(0, colonIndex).trim();
const secret = credentials.substring(colonIndex + 1).trim();
const user = await authenticateWithAppPassword(username, secret);
if (user) {
req.user = user;
req.session = { user };
return next();
}
} catch (error) {
logger.error('Basic auth error:', error.message);
}
}
const appPassword = apiKey || passwordParam;
if (appPassword && userParam) {
logger.debug('App password and username present', { userParam });
const user = await authenticateWithAppPassword(userParam, appPassword);
if (user) {
req.user = user;
req.session = { user };
return next();
}
}
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 = () => {
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,
isAppAuthenticated,
isAppQueryAuthenticated,
lookupUser,
clearUserCache,
getUserCacheStats,
removeUserFromCache,
getEnvironment,
};