Compare commits
2 Commits
7a398c79a1
...
289673813a
| Author | SHA1 | Date | |
|---|---|---|---|
| 289673813a | |||
| 840aa0781b |
19
config.json
19
config.json
@ -17,7 +17,11 @@
|
|||||||
"mongo": {
|
"mongo": {
|
||||||
"url": "mongodb://127.0.0.1:27017/farmcontrol"
|
"url": "mongodb://127.0.0.1:27017/farmcontrol"
|
||||||
},
|
},
|
||||||
"redis": { "host": "localhost", "port": 6379, "password": "" }
|
"redis": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 6379,
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"otpExpiryMins": 0.5
|
"otpExpiryMins": 0.5
|
||||||
},
|
},
|
||||||
@ -39,7 +43,11 @@
|
|||||||
"mongo": {
|
"mongo": {
|
||||||
"url": "mongodb://127.0.0.1:27017/farmcontrol-test"
|
"url": "mongodb://127.0.0.1:27017/farmcontrol-test"
|
||||||
},
|
},
|
||||||
"redis": { "host": "localhost", "port": 6379, "password": "" }
|
"redis": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 6379,
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"otpExpiryMins": 0.5
|
"otpExpiryMins": 0.5
|
||||||
},
|
},
|
||||||
@ -60,7 +68,12 @@
|
|||||||
"database": {
|
"database": {
|
||||||
"mongo": {
|
"mongo": {
|
||||||
"url": "mongodb://localhost:27017/farmcontrol"
|
"url": "mongodb://localhost:27017/farmcontrol"
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 6379,
|
||||||
|
"password": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
133
src/auth/auth.js
133
src/auth/auth.js
@ -1,9 +1,7 @@
|
|||||||
// auth.js - Keycloak authentication handler
|
// auth.js - Redis session authentication (shared with API)
|
||||||
import axios from 'axios';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
// Load configuration
|
|
||||||
import { loadConfig } from '../config.js';
|
import { loadConfig } from '../config.js';
|
||||||
|
import { redisServer } from '../database/redis.js';
|
||||||
import { editObject, getObject, listObjects } from '../database/database.js';
|
import { editObject, getObject, listObjects } from '../database/database.js';
|
||||||
import { hostModel } from '../database/schemas/management/host.schema.js';
|
import { hostModel } from '../database/schemas/management/host.schema.js';
|
||||||
import { userModel } from '../database/schemas/management/user.schema.js';
|
import { userModel } from '../database/schemas/management/user.schema.js';
|
||||||
@ -14,123 +12,69 @@ const config = loadConfig();
|
|||||||
const logger = log4js.getLogger('Auth');
|
const logger = log4js.getLogger('Auth');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
export class KeycloakAuth {
|
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() {
|
constructor() {
|
||||||
this.config = config.auth;
|
this.config = config.auth;
|
||||||
this.tokenCache = new Map(); // Cache for verified tokens
|
this.tokenCache = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a token with Keycloak server
|
|
||||||
async verifyToken(token) {
|
async verifyToken(token) {
|
||||||
// Check cache first
|
if (!token) return { valid: false };
|
||||||
|
|
||||||
if (this.tokenCache.has(token)) {
|
if (this.tokenCache.has(token)) {
|
||||||
const cachedInfo = this.tokenCache.get(token);
|
const cached = this.tokenCache.get(token);
|
||||||
if (cachedInfo.expiresAt > Date.now()) {
|
if (cached.expiresAt > Date.now()) {
|
||||||
return { valid: true, user: cachedInfo.user };
|
return { valid: true, user: cached.user };
|
||||||
} else {
|
|
||||||
// Token expired, remove from cache
|
|
||||||
this.tokenCache.delete(token);
|
|
||||||
}
|
}
|
||||||
|
this.tokenCache.delete(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify token with Keycloak introspection endpoint
|
const key = SESSION_KEY_PREFIX + token;
|
||||||
const response = await axios.post(
|
const session = await redisServer.getKey(key);
|
||||||
`${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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const introspection = response.data;
|
if (!session || !session.user) {
|
||||||
|
logger.info('Session not found or invalid');
|
||||||
if (!introspection.active) {
|
return { valid: false };
|
||||||
logger.info('Token is not active');
|
}
|
||||||
|
|
||||||
|
if (session.expiresAt && session.expiresAt <= Date.now()) {
|
||||||
|
logger.info('Session expired');
|
||||||
return { valid: false };
|
return { valid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify required roles if configured
|
|
||||||
if (this.config.requiredRoles && this.config.requiredRoles.length > 0) {
|
if (this.config.requiredRoles && this.config.requiredRoles.length > 0) {
|
||||||
const hasRequiredRole = this.checkRoles(
|
const roles = session.user?.roles || [];
|
||||||
introspection,
|
const hasRole = this.config.requiredRoles.some((r) => roles.includes(r));
|
||||||
this.config.requiredRoles
|
if (!hasRole) {
|
||||||
);
|
|
||||||
if (!hasRequiredRole) {
|
|
||||||
logger.info("User doesn't have required roles");
|
logger.info("User doesn't have required roles");
|
||||||
return { valid: false };
|
return { valid: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse token to extract user info
|
this.tokenCache.set(token, {
|
||||||
const decodedToken = jwt.decode(token);
|
expiresAt: session.expiresAt,
|
||||||
const decodedUser = {
|
user: session.user
|
||||||
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 }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cache the verified token
|
return { valid: true, user: session.user };
|
||||||
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
|
|
||||||
this.tokenCache.set(token, { expiresAt, user: user[0] });
|
|
||||||
|
|
||||||
return { valid: true, user: user[0] };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Token verification error:', error.message);
|
logger.error('Session verification error:', error.message);
|
||||||
return { valid: false };
|
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 {
|
export class CodeAuth {
|
||||||
// Verify a code with the database
|
|
||||||
async verifyCode(id, authCode) {
|
async verifyCode(id, authCode) {
|
||||||
try {
|
try {
|
||||||
logger.trace('Verifying code:', { id, authCode });
|
logger.trace('Verifying code:', { id, authCode });
|
||||||
@ -212,12 +156,9 @@ export class CodeAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket.IO middleware for authentication
|
|
||||||
export function createAuthMiddleware(socketUser) {
|
export function createAuthMiddleware(socketUser) {
|
||||||
return async (packet, next) => {
|
return async (packet, next) => {
|
||||||
const [event] = packet; // event name is always first element
|
const [event] = packet;
|
||||||
|
|
||||||
// Allow the 'authenticate' event through without checks
|
|
||||||
|
|
||||||
if (event === 'authenticate') {
|
if (event === 'authenticate') {
|
||||||
next();
|
next();
|
||||||
|
|||||||
@ -35,89 +35,243 @@ import { salesOrderModel } from './sales/salesorder.schema.js';
|
|||||||
|
|
||||||
// Map prefixes to models and id fields
|
// Map prefixes to models and id fields
|
||||||
export const models = {
|
export const models = {
|
||||||
PRN: { model: printerModel, idField: '_id', type: 'printer', referenceField: '_reference' },
|
PRN: {
|
||||||
FIL: { model: filamentModel, idField: '_id', type: 'filament', referenceField: '_reference' },
|
model: printerModel,
|
||||||
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodeFile', referenceField: '_reference' },
|
idField: '_id',
|
||||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference' },
|
type: 'printer',
|
||||||
PRT: { model: partModel, idField: '_id', type: 'part', referenceField: '_reference' },
|
referenceField: '_reference',
|
||||||
PRD: { model: productModel, idField: '_id', type: 'product', referenceField: '_reference' },
|
label: 'Printer',
|
||||||
VEN: { model: vendorModel, idField: '_id', type: 'vendor', referenceField: '_reference' },
|
},
|
||||||
SJB: { model: subJobModel, idField: '_id', type: 'subJob', referenceField: '_reference' },
|
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',
|
||||||
|
},
|
||||||
FLS: {
|
FLS: {
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'filamentStock',
|
type: 'filamentStock',
|
||||||
referenceField: '_reference',
|
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: {
|
NTF: {
|
||||||
model: notificationModel,
|
model: notificationModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'notification',
|
type: 'notification',
|
||||||
|
label: 'Notification',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
ONF: {
|
ONF: {
|
||||||
model: userNotifierModel,
|
model: userNotifierModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'userNotifier',
|
type: 'userNotifier',
|
||||||
|
label: 'User Notifier',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DSZ: {
|
DSZ: {
|
||||||
model: documentSizeModel,
|
model: documentSizeModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentSize',
|
type: 'documentSize',
|
||||||
|
label: 'Document Size',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DTP: {
|
DTP: {
|
||||||
model: documentTemplateModel,
|
model: documentTemplateModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentTemplate',
|
type: 'documentTemplate',
|
||||||
|
label: 'Document Template',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DPR: {
|
DPR: {
|
||||||
model: documentPrinterModel,
|
model: documentPrinterModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentPrinter',
|
type: 'documentPrinter',
|
||||||
|
label: 'Document Printer',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DJB: {
|
DJB: {
|
||||||
model: documentJobModel,
|
model: documentJobModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentJob',
|
type: 'documentJob',
|
||||||
|
label: 'Document Job',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
HST: { model: hostModel, idField: '_id', type: 'host', referenceField: '_reference' },
|
HST: {
|
||||||
FLE: { model: fileModel, idField: '_id', type: 'file', referenceField: '_reference' },
|
model: hostModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'host',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Host',
|
||||||
|
},
|
||||||
|
FLE: {
|
||||||
|
model: fileModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'file',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'File',
|
||||||
|
},
|
||||||
POR: {
|
POR: {
|
||||||
model: purchaseOrderModel,
|
model: purchaseOrderModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'purchaseOrder',
|
type: 'purchaseOrder',
|
||||||
|
label: 'Purchase Order',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
ODI: {
|
ODI: {
|
||||||
model: orderItemModel,
|
model: orderItemModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'orderItem',
|
type: 'orderItem',
|
||||||
|
label: 'Order Item',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
COS: {
|
COS: {
|
||||||
model: courierServiceModel,
|
model: courierServiceModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'courierService',
|
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',
|
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