All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
- 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.
161 lines
5.0 KiB
JavaScript
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.'));
|
|
};
|
|
}
|