Tom Butcher aad49bfcfd
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
Add formatTraceData utility and integrate into logging
- Introduced a new utility function `formatTraceData` to standardize the formatting of trace data for logging purposes.
- Updated various modules including auth, database, and stats manager to utilize `formatTraceData` for improved log clarity and consistency.
- Added new schemas for printer profiles and filament profiles to enhance the data model.
- Implemented tests for session authentication to ensure robust functionality and error handling.
2026-07-20 02:11:56 +01:00

161 lines
5.0 KiB
JavaScript

// auth.js - Redis session authentication (shared with API)
import log4js from 'log4js';
import { loadConfig } from '../config.js';
import { redisServer } from '../database/redis.js';
import { editObject, getObject, listObjects } from '../database/database.js';
import { hostModel } from '../database/schemas/management/host.schema.js';
import { userModel } from '../database/schemas/management/user.schema.js';
import { formatTraceData, generateAuthCode } from '../utils.js';
const config = loadConfig();
const logger = log4js.getLogger('Auth');
logger.level = config.server.logLevel;
const SESSION_KEY_PREFIX = 'session:';
/**
* SessionAuth - validates tokens by looking up Redis session (created by API after Keycloak login).
* Same session key format as API: session:{sessionToken}
*/
export class SessionAuth {
constructor() {
this.config = config.auth;
}
async verifyToken(token) {
if (!token) return { valid: false };
try {
const key = SESSION_KEY_PREFIX + token;
const session = await redisServer.getKey(key);
if (!session || !session.user) {
logger.info('Session not found or invalid');
return { valid: false };
}
if (session.expiresAt && session.expiresAt <= Date.now()) {
logger.info('Session expired');
return { valid: false };
}
if (this.config.requiredRoles && this.config.requiredRoles.length > 0) {
const roles = session.user?.roles || [];
const hasRole = this.config.requiredRoles.some((r) => roles.includes(r));
if (!hasRole) {
logger.info("User doesn't have required roles");
return { valid: false };
}
}
return { valid: true, user: session.user };
} catch (error) {
logger.error('Session verification error:', error.message);
return { valid: false };
}
}
}
/** @deprecated Use SessionAuth - kept for backward compatibility */
export class KeycloakAuth extends SessionAuth {}
export class CodeAuth {
async verifyCode(id, authCode) {
try {
logger.trace(`Verifying code: ${formatTraceData({ id, authCode })}`);
const host = await getObject({ model: hostModel, id, cached: true });
logger.trace(`Host retrieved: ${formatTraceData(host)}`);
if (host == undefined) {
const error = 'Host not found.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.active == false) {
const error = 'Host not active.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.authCode == undefined || host.authCode == '') {
const error = 'No authCode on database.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.authCode != authCode) {
const error = 'authCode does not match.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
logger.trace(`Code verification successful: ${formatTraceData(host)}`);
return { valid: true, host: host };
} catch (error) {
logger.error('Code verification error:', error.message);
return { valid: false };
}
}
async verifyOtp(otp) {
try {
const hosts = await listObjects({
model: hostModel,
filter: { otp: otp },
cached: false
});
const host = hosts[0];
if (host == undefined) {
const error = `No host found with OTP: ${otp}`;
logger.warn(error);
return { valid: false, error: error };
}
const id = host._id.toString();
if (host.active == false) {
const error = 'Host is not active.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.otp == undefined) {
const error = 'No OTP on database.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.otpExpiresAt == undefined) {
const error = 'No OTP expiry.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.otpExpiresAt < Date.now()) {
const error = 'OTP expired.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
const authCodeHost = await editObject({
model: hostModel,
id: id,
updateData: { authCode: generateAuthCode() }
});
logger.info('Host found with OTP:', otp);
return { valid: true, host: authCodeHost };
} catch (error) {
logger.error('OTP verification error:', error.message);
return { valid: false, error: error.message };
}
}
}
export function createAuthMiddleware(socketUser) {
return async (packet, next) => {
const [event] = packet;
if (event === 'authenticate') {
next();
return;
}
if (socketUser.authenticated) {
next();
return;
}
return next(new Error('Authentication is required.'));
};
}