Compare commits
No commits in common. "289673813a6581340f82a073c842c81026485767" and "7a398c79a1d751d90323fb1250a17313ea6ec76b" have entirely different histories.
289673813a
...
7a398c79a1
19
config.json
19
config.json
@ -17,11 +17,7 @@
|
||||
"mongo": {
|
||||
"url": "mongodb://127.0.0.1:27017/farmcontrol"
|
||||
},
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"password": ""
|
||||
}
|
||||
"redis": { "host": "localhost", "port": 6379, "password": "" }
|
||||
},
|
||||
"otpExpiryMins": 0.5
|
||||
},
|
||||
@ -43,11 +39,7 @@
|
||||
"mongo": {
|
||||
"url": "mongodb://127.0.0.1:27017/farmcontrol-test"
|
||||
},
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"password": ""
|
||||
}
|
||||
"redis": { "host": "localhost", "port": 6379, "password": "" }
|
||||
},
|
||||
"otpExpiryMins": 0.5
|
||||
},
|
||||
@ -68,12 +60,7 @@
|
||||
"database": {
|
||||
"mongo": {
|
||||
"url": "mongodb://localhost:27017/farmcontrol"
|
||||
},
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
133
src/auth/auth.js
133
src/auth/auth.js
@ -1,7 +1,9 @@
|
||||
// auth.js - Redis session authentication (shared with API)
|
||||
// auth.js - Keycloak authentication handler
|
||||
import axios from 'axios';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import log4js from 'log4js';
|
||||
// Load configuration
|
||||
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';
|
||||
@ -12,69 +14,123 @@ 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 {
|
||||
export class KeycloakAuth {
|
||||
constructor() {
|
||||
this.config = config.auth;
|
||||
this.tokenCache = new Map();
|
||||
this.tokenCache = new Map(); // Cache for verified tokens
|
||||
}
|
||||
|
||||
// Verify a token with Keycloak server
|
||||
async verifyToken(token) {
|
||||
if (!token) return { valid: false };
|
||||
|
||||
// Check cache first
|
||||
if (this.tokenCache.has(token)) {
|
||||
const cached = this.tokenCache.get(token);
|
||||
if (cached.expiresAt > Date.now()) {
|
||||
return { valid: true, user: cached.user };
|
||||
const cachedInfo = this.tokenCache.get(token);
|
||||
if (cachedInfo.expiresAt > Date.now()) {
|
||||
return { valid: true, user: cachedInfo.user };
|
||||
} else {
|
||||
// Token expired, remove from cache
|
||||
this.tokenCache.delete(token);
|
||||
}
|
||||
this.tokenCache.delete(token);
|
||||
}
|
||||
|
||||
try {
|
||||
const key = SESSION_KEY_PREFIX + token;
|
||||
const session = await redisServer.getKey(key);
|
||||
// Verify token with Keycloak introspection endpoint
|
||||
const response = await axios.post(
|
||||
`${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
||||
new URLSearchParams({
|
||||
token,
|
||||
client_id: this.config.keycloak.clientId,
|
||||
client_secret: this.config.keycloak.clientSecret
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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');
|
||||
const introspection = response.data;
|
||||
|
||||
if (!introspection.active) {
|
||||
logger.info('Token is not active');
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
// Verify required roles if configured
|
||||
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) {
|
||||
const hasRequiredRole = this.checkRoles(
|
||||
introspection,
|
||||
this.config.requiredRoles
|
||||
);
|
||||
if (!hasRequiredRole) {
|
||||
logger.info("User doesn't have required roles");
|
||||
return { valid: false };
|
||||
}
|
||||
}
|
||||
|
||||
this.tokenCache.set(token, {
|
||||
expiresAt: session.expiresAt,
|
||||
user: session.user
|
||||
// Parse token to extract user info
|
||||
const decodedToken = jwt.decode(token);
|
||||
const decodedUser = {
|
||||
id: decodedToken.sub,
|
||||
username: decodedToken.preferred_username,
|
||||
email: decodedToken.email,
|
||||
name: decodedToken.name,
|
||||
roles: this.extractRoles(decodedToken)
|
||||
};
|
||||
|
||||
const user = await listObjects({
|
||||
model: userModel,
|
||||
filter: { username: decodedUser.username }
|
||||
});
|
||||
|
||||
return { valid: true, user: session.user };
|
||||
// Cache the verified token
|
||||
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
|
||||
this.tokenCache.set(token, { expiresAt, user: user[0] });
|
||||
|
||||
return { valid: true, user: user[0] };
|
||||
} catch (error) {
|
||||
logger.error('Session verification error:', error.message);
|
||||
logger.error('Token verification error:', error.message);
|
||||
return { valid: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Extract roles from token
|
||||
extractRoles(token) {
|
||||
const roles = [];
|
||||
|
||||
// Extract realm roles
|
||||
if (token.realm_access && token.realm_access.roles) {
|
||||
roles.push(...token.realm_access.roles);
|
||||
}
|
||||
|
||||
// Extract client roles
|
||||
if (token.resource_access) {
|
||||
for (const client in token.resource_access) {
|
||||
if (token.resource_access[client].roles) {
|
||||
roles.push(
|
||||
...token.resource_access[client].roles.map(
|
||||
role => `${client}:${role}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
// Check if user has required roles
|
||||
checkRoles(tokenInfo, requiredRoles) {
|
||||
// Extract roles from token
|
||||
const userRoles = this.extractRoles(tokenInfo);
|
||||
|
||||
// Check if user has any of the required roles
|
||||
return requiredRoles.some(role => userRoles.includes(role));
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use SessionAuth - kept for backward compatibility */
|
||||
export class KeycloakAuth extends SessionAuth {}
|
||||
|
||||
export class CodeAuth {
|
||||
// Verify a code with the database
|
||||
async verifyCode(id, authCode) {
|
||||
try {
|
||||
logger.trace('Verifying code:', { id, authCode });
|
||||
@ -156,9 +212,12 @@ export class CodeAuth {
|
||||
}
|
||||
}
|
||||
|
||||
// Socket.IO middleware for authentication
|
||||
export function createAuthMiddleware(socketUser) {
|
||||
return async (packet, next) => {
|
||||
const [event] = packet;
|
||||
const [event] = packet; // event name is always first element
|
||||
|
||||
// Allow the 'authenticate' event through without checks
|
||||
|
||||
if (event === 'authenticate') {
|
||||
next();
|
||||
|
||||
@ -35,243 +35,89 @@ import { salesOrderModel } from './sales/salesorder.schema.js';
|
||||
|
||||
// Map prefixes to models and id fields
|
||||
export const models = {
|
||||
PRN: {
|
||||
model: printerModel,
|
||||
idField: '_id',
|
||||
type: 'printer',
|
||||
referenceField: '_reference',
|
||||
label: 'Printer',
|
||||
},
|
||||
FIL: {
|
||||
model: filamentModel,
|
||||
idField: '_id',
|
||||
type: 'filament',
|
||||
referenceField: '_reference',
|
||||
label: 'Filament',
|
||||
},
|
||||
GCF: {
|
||||
model: gcodeFileModel,
|
||||
idField: '_id',
|
||||
type: 'gcodeFile',
|
||||
referenceField: '_reference',
|
||||
label: 'G-Code File',
|
||||
},
|
||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference', label: 'Job' },
|
||||
PRT: {
|
||||
model: partModel,
|
||||
idField: '_id',
|
||||
type: 'part',
|
||||
referenceField: '_reference',
|
||||
label: 'Part',
|
||||
},
|
||||
PRD: {
|
||||
model: productModel,
|
||||
idField: '_id',
|
||||
type: 'product',
|
||||
referenceField: '_reference',
|
||||
label: 'Product',
|
||||
},
|
||||
VEN: {
|
||||
model: vendorModel,
|
||||
idField: '_id',
|
||||
type: 'vendor',
|
||||
referenceField: '_reference',
|
||||
label: 'Vendor',
|
||||
},
|
||||
SJB: {
|
||||
model: subJobModel,
|
||||
idField: '_id',
|
||||
type: 'subJob',
|
||||
referenceField: '_reference',
|
||||
label: 'Sub Job',
|
||||
},
|
||||
PRN: { model: printerModel, idField: '_id', type: 'printer', referenceField: '_reference' },
|
||||
FIL: { model: filamentModel, idField: '_id', type: 'filament', referenceField: '_reference' },
|
||||
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodeFile', referenceField: '_reference' },
|
||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference' },
|
||||
PRT: { model: partModel, idField: '_id', type: 'part', referenceField: '_reference' },
|
||||
PRD: { model: productModel, idField: '_id', type: 'product', referenceField: '_reference' },
|
||||
VEN: { model: vendorModel, idField: '_id', type: 'vendor', referenceField: '_reference' },
|
||||
SJB: { model: subJobModel, idField: '_id', type: 'subJob', referenceField: '_reference' },
|
||||
FLS: {
|
||||
model: filamentStockModel,
|
||||
idField: '_id',
|
||||
type: 'filamentStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Filament Stock',
|
||||
},
|
||||
SEV: {
|
||||
model: stockEventModel,
|
||||
idField: '_id',
|
||||
type: 'stockEvent',
|
||||
referenceField: '_reference',
|
||||
label: 'Stock Event',
|
||||
},
|
||||
SAU: {
|
||||
model: stockAuditModel,
|
||||
idField: '_id',
|
||||
type: 'stockAudit',
|
||||
referenceField: '_reference',
|
||||
label: 'Stock Audit',
|
||||
},
|
||||
PTS: {
|
||||
model: partStockModel,
|
||||
idField: '_id',
|
||||
type: 'partStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Part Stock',
|
||||
},
|
||||
PDS: {
|
||||
model: null,
|
||||
idField: '_id',
|
||||
type: 'productStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Product Stock',
|
||||
}, // No productStockModel found
|
||||
ADL: {
|
||||
model: auditLogModel,
|
||||
idField: '_id',
|
||||
type: 'auditLog',
|
||||
referenceField: '_reference',
|
||||
label: 'Audit Log',
|
||||
},
|
||||
USR: {
|
||||
model: userModel,
|
||||
idField: '_id',
|
||||
type: 'user',
|
||||
referenceField: '_reference',
|
||||
label: 'User',
|
||||
},
|
||||
NTY: {
|
||||
model: noteTypeModel,
|
||||
idField: '_id',
|
||||
type: 'noteType',
|
||||
referenceField: '_reference',
|
||||
label: 'Note Type',
|
||||
},
|
||||
NTE: {
|
||||
model: noteModel,
|
||||
idField: '_id',
|
||||
type: 'note',
|
||||
referenceField: '_reference',
|
||||
label: 'Note',
|
||||
},
|
||||
SEV: { model: stockEventModel, idField: '_id', type: 'stockEvent', referenceField: '_reference' },
|
||||
SAU: { model: stockAuditModel, idField: '_id', type: 'stockAudit', referenceField: '_reference' },
|
||||
PTS: { model: partStockModel, idField: '_id', type: 'partStock', referenceField: '_reference' },
|
||||
PDS: { model: null, idField: '_id', type: 'productStock', referenceField: '_reference' }, // No productStockModel found
|
||||
ADL: { model: auditLogModel, idField: '_id', type: 'auditLog', referenceField: '_reference' },
|
||||
USR: { model: userModel, idField: '_id', type: 'user', referenceField: '_reference' },
|
||||
NTY: { model: noteTypeModel, idField: '_id', type: 'noteType', referenceField: '_reference' },
|
||||
NTE: { model: noteModel, idField: '_id', type: 'note', referenceField: '_reference' },
|
||||
NTF: {
|
||||
model: notificationModel,
|
||||
idField: '_id',
|
||||
type: 'notification',
|
||||
label: 'Notification',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
ONF: {
|
||||
model: userNotifierModel,
|
||||
idField: '_id',
|
||||
type: 'userNotifier',
|
||||
label: 'User Notifier',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DSZ: {
|
||||
model: documentSizeModel,
|
||||
idField: '_id',
|
||||
type: 'documentSize',
|
||||
label: 'Document Size',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DTP: {
|
||||
model: documentTemplateModel,
|
||||
idField: '_id',
|
||||
type: 'documentTemplate',
|
||||
label: 'Document Template',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DPR: {
|
||||
model: documentPrinterModel,
|
||||
idField: '_id',
|
||||
type: 'documentPrinter',
|
||||
label: 'Document Printer',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DJB: {
|
||||
model: documentJobModel,
|
||||
idField: '_id',
|
||||
type: 'documentJob',
|
||||
label: 'Document Job',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
HST: {
|
||||
model: hostModel,
|
||||
idField: '_id',
|
||||
type: 'host',
|
||||
referenceField: '_reference',
|
||||
label: 'Host',
|
||||
},
|
||||
FLE: {
|
||||
model: fileModel,
|
||||
idField: '_id',
|
||||
type: 'file',
|
||||
referenceField: '_reference',
|
||||
label: 'File',
|
||||
},
|
||||
HST: { model: hostModel, idField: '_id', type: 'host', referenceField: '_reference' },
|
||||
FLE: { model: fileModel, idField: '_id', type: 'file', referenceField: '_reference' },
|
||||
POR: {
|
||||
model: purchaseOrderModel,
|
||||
idField: '_id',
|
||||
type: 'purchaseOrder',
|
||||
label: 'Purchase Order',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
ODI: {
|
||||
model: orderItemModel,
|
||||
idField: '_id',
|
||||
type: 'orderItem',
|
||||
label: 'Order Item',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
COS: {
|
||||
model: courierServiceModel,
|
||||
idField: '_id',
|
||||
type: 'courierService',
|
||||
label: 'Courier Service',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
COR: {
|
||||
model: courierModel,
|
||||
idField: '_id',
|
||||
type: 'courier',
|
||||
label: 'Courier',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
TXR: {
|
||||
model: taxRateModel,
|
||||
idField: '_id',
|
||||
type: 'taxRate',
|
||||
label: 'Tax Rate',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
TXD: {
|
||||
model: taxRecordModel,
|
||||
idField: '_id',
|
||||
type: 'taxRecord',
|
||||
label: 'Tax Record',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
SHP: {
|
||||
model: shipmentModel,
|
||||
idField: '_id',
|
||||
type: 'shipment',
|
||||
label: 'Shipment',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
INV: {
|
||||
model: invoiceModel,
|
||||
idField: '_id',
|
||||
type: 'invoice',
|
||||
label: 'Invoice',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
CLI: {
|
||||
model: clientModel,
|
||||
idField: '_id',
|
||||
type: 'client',
|
||||
label: 'Client',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
SOR: {
|
||||
model: salesOrderModel,
|
||||
idField: '_id',
|
||||
type: 'salesOrder',
|
||||
label: 'Sales Order',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' },
|
||||
TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' },
|
||||
TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' },
|
||||
SHP: { model: shipmentModel, idField: '_id', type: 'shipment', referenceField: '_reference' },
|
||||
INV: { model: invoiceModel, idField: '_id', type: 'invoice', referenceField: '_reference' },
|
||||
CLI: { model: clientModel, idField: '_id', type: 'client', referenceField: '_reference' },
|
||||
SOR: { model: salesOrderModel, idField: '_id', type: 'salesOrder', referenceField: '_reference' },
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user