Implemented multiple app passwords.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit is contained in:
parent
eaa8bf4836
commit
8ad3d3da5c
@ -60,7 +60,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"syncModelsWithWS": "node fcdev.js",
|
"syncModelsWithWS": "node fcdev.js",
|
||||||
"watch:schemas": "nodemon --config nodemon.schemas.json",
|
"watch:schemas": "nodemon --config nodemon.schemas.json",
|
||||||
"dev": "concurrently --names \"API,SCHEMAS\" --prefix-colors \"cyan,yellow\" \"nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js\" \"nodemon --config nodemon.schemas.json\"",
|
"dev": "concurrently --kill-others --names \"API,SCHEMAS\" --prefix-colors \"cyan,yellow\" \"nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js\" \"nodemon --config nodemon.schemas.json\"",
|
||||||
"dev:api": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js",
|
"dev:api": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js",
|
||||||
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
||||||
"seed": "node src/mongo/seedData.js",
|
"seed": "node src/mongo/seedData.js",
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { listObjects } from './database.js';
|
|||||||
const logger = log4js.getLogger('DatabaseOData');
|
const logger = log4js.getLogger('DatabaseOData');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
const EXCLUDED_PATHS = ['appPasswordHash', '__v'];
|
const EXCLUDED_PATHS = ['appPasswordHash', 'secret', '__v'];
|
||||||
|
|
||||||
/** Check if path is an embedded object (has nested schema, not ObjectId ref). Excludes arrays. */
|
/** Check if path is an embedded object (has nested schema, not ObjectId ref). Excludes arrays. */
|
||||||
function isEmbeddedObject(path) {
|
function isEmbeddedObject(path) {
|
||||||
|
|||||||
22
src/database/schemas/management/apppassword.schema.js
Normal file
22
src/database/schemas/management/apppassword.schema.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const appPasswordSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
user: { type: Schema.Types.ObjectId, ref: 'user', required: true },
|
||||||
|
active: { type: Boolean, required: true, default: true },
|
||||||
|
secret: { type: String, required: true, select: false },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
appPasswordSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
appPasswordSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const appPasswordModel = mongoose.model('appPassword', appPasswordSchema);
|
||||||
@ -14,6 +14,7 @@ import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
|||||||
import { partStockModel } from './inventory/partstock.schema.js';
|
import { partStockModel } from './inventory/partstock.schema.js';
|
||||||
import { auditLogModel } from './management/auditlog.schema.js';
|
import { auditLogModel } from './management/auditlog.schema.js';
|
||||||
import { userModel } from './management/user.schema.js';
|
import { userModel } from './management/user.schema.js';
|
||||||
|
import { appPasswordModel } from './management/apppassword.schema.js';
|
||||||
import { noteTypeModel } from './management/notetype.schema.js';
|
import { noteTypeModel } from './management/notetype.schema.js';
|
||||||
import { noteModel } from './misc/note.schema.js';
|
import { noteModel } from './misc/note.schema.js';
|
||||||
import { notificationModel } from './misc/notification.schema.js';
|
import { notificationModel } from './misc/notification.schema.js';
|
||||||
@ -134,6 +135,13 @@ export const models = {
|
|||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
label: 'User',
|
label: 'User',
|
||||||
},
|
},
|
||||||
|
APP: {
|
||||||
|
model: appPasswordModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'appPassword',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'App Password',
|
||||||
|
},
|
||||||
NTY: {
|
NTY: {
|
||||||
model: noteTypeModel,
|
model: noteTypeModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { dbConnect } from './database/mongo.js';
|
|||||||
import {
|
import {
|
||||||
authRoutes,
|
authRoutes,
|
||||||
userRoutes,
|
userRoutes,
|
||||||
|
appPasswordRoutes,
|
||||||
fileRoutes,
|
fileRoutes,
|
||||||
printerRoutes,
|
printerRoutes,
|
||||||
jobRoutes,
|
jobRoutes,
|
||||||
@ -114,6 +115,7 @@ app.get('/', function (req, res) {
|
|||||||
|
|
||||||
app.use('/auth', authRoutes);
|
app.use('/auth', authRoutes);
|
||||||
app.use('/users', userRoutes);
|
app.use('/users', userRoutes);
|
||||||
|
app.use('/apppasswords', appPasswordRoutes);
|
||||||
app.use('/files', fileRoutes);
|
app.use('/files', fileRoutes);
|
||||||
app.use('/spotlight', spotlightRoutes);
|
app.use('/spotlight', spotlightRoutes);
|
||||||
app.use('/printers', printerRoutes);
|
app.use('/printers', printerRoutes);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import log4js from 'log4js';
|
|||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { userModel } from './database/schemas/management/user.schema.js';
|
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 { getObject } from './database/database.js';
|
||||||
import { hostModel } from './database/schemas/management/host.schema.js';
|
import { hostModel } from './database/schemas/management/host.schema.js';
|
||||||
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
||||||
@ -79,28 +80,6 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try HTTP Basic Auth (username + app password)
|
|
||||||
if (authHeader && authHeader.startsWith('Basic ')) {
|
|
||||||
try {
|
|
||||||
logger.debug('Basic auth header:', authHeader);
|
|
||||||
const base64Credentials = authHeader.substring(6);
|
|
||||||
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
||||||
const [username, password] = credentials.split(':');
|
|
||||||
logger.debug('Basic auth credentials:', { username, password });
|
|
||||||
if (username && password) {
|
|
||||||
const user = await userModel.findOne({ username }).select('+appPasswordHash');
|
|
||||||
if (user?.appPasswordHash && (await bcrypt.compare(password, user.appPasswordHash))) {
|
|
||||||
user.appPasswordHash = undefined; // don't expose hash downstream
|
|
||||||
req.user = user;
|
|
||||||
req.session = { user };
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Basic auth error:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostId = req.headers['x-host-id'];
|
const hostId = req.headers['x-host-id'];
|
||||||
const authCode = req.headers['x-auth-code'];
|
const authCode = req.headers['x-auth-code'];
|
||||||
if (hostId && authCode) {
|
if (hostId && authCode) {
|
||||||
@ -113,6 +92,41 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAppAuthenticated = async (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
|
// Try 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();
|
||||||
|
if (username && secret) {
|
||||||
|
const user = await userModel.findOne({ username });
|
||||||
|
if (user) {
|
||||||
|
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))) {
|
||||||
|
req.user = user;
|
||||||
|
req.session = { user };
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Basic auth error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
|
};
|
||||||
|
|
||||||
const clearUserCache = () => {
|
const clearUserCache = () => {
|
||||||
userCache.flushAll();
|
userCache.flushAll();
|
||||||
logger.info('User cache cleared');
|
logger.info('User cache cleared');
|
||||||
@ -129,6 +143,7 @@ const removeUserFromCache = (username) => {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
isAppAuthenticated,
|
||||||
lookupUser,
|
lookupUser,
|
||||||
clearUserCache,
|
clearUserCache,
|
||||||
getUserCacheStats,
|
getUserCacheStats,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import userRoutes from './management/users.js';
|
import userRoutes from './management/users.js';
|
||||||
|
import appPasswordRoutes from './management/apppasswords.js';
|
||||||
import fileRoutes from './management/files.js';
|
import fileRoutes from './management/files.js';
|
||||||
import authRoutes from './misc/auth.js';
|
import authRoutes from './misc/auth.js';
|
||||||
import printerRoutes from './production/printers.js';
|
import printerRoutes from './production/printers.js';
|
||||||
@ -40,6 +41,7 @@ import odataRoutes from './misc/odata.js';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
userRoutes,
|
userRoutes,
|
||||||
|
appPasswordRoutes,
|
||||||
fileRoutes,
|
fileRoutes,
|
||||||
authRoutes,
|
authRoutes,
|
||||||
printerRoutes,
|
printerRoutes,
|
||||||
|
|||||||
61
src/routes/management/apppasswords.js
Normal file
61
src/routes/management/apppasswords.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { getFilter, convertPropertiesString } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
listAppPasswordsRouteHandler,
|
||||||
|
getAppPasswordRouteHandler,
|
||||||
|
editAppPasswordRouteHandler,
|
||||||
|
newAppPasswordRouteHandler,
|
||||||
|
deleteAppPasswordRouteHandler,
|
||||||
|
listAppPasswordsByPropertiesRouteHandler,
|
||||||
|
getAppPasswordStatsRouteHandler,
|
||||||
|
getAppPasswordHistoryRouteHandler,
|
||||||
|
regenerateSecretRouteHandler,
|
||||||
|
} from '../../services/management/apppasswords.js';
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
|
const allowedFilters = ['_id', 'name', 'user', 'active', 'user._id'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
|
listAppPasswordsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
|
const properties = convertPropertiesString(req.query.properties);
|
||||||
|
const allowedFilters = ['name', 'user', 'active'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
|
const masterFilter = req.query.masterFilter ? JSON.parse(req.query.masterFilter) : {};
|
||||||
|
listAppPasswordsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
|
newAppPasswordRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, (req, res) => {
|
||||||
|
getAppPasswordStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, (req, res) => {
|
||||||
|
getAppPasswordHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:id/regenerateSecret', isAuthenticated, async (req, res) => {
|
||||||
|
regenerateSecretRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, (req, res) => {
|
||||||
|
getAppPasswordRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
editAppPasswordRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteAppPasswordRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
import { isAppAuthenticated } from '../../keycloak.js';
|
||||||
import { listODataRouteHandler, metadataODataRouteHandler } from '../../services/misc/odata.js';
|
import { listODataRouteHandler, metadataODataRouteHandler } from '../../services/misc/odata.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/$metadata', isAuthenticated, metadataODataRouteHandler);
|
router.get('/$metadata', isAppAuthenticated, metadataODataRouteHandler);
|
||||||
router.get('/:objectType', isAuthenticated, listODataRouteHandler);
|
router.get('/:objectType', isAppAuthenticated, listODataRouteHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
229
src/services/management/apppasswords.js
Normal file
229
src/services/management/apppasswords.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { appPasswordModel } from '../../database/schemas/management/apppassword.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import {
|
||||||
|
listObjects,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
newObject,
|
||||||
|
deleteObject,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
} from '../../database/database.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('AppPasswords');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export const listAppPasswordsRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
|
) => {
|
||||||
|
const result = await listObjects({
|
||||||
|
model: appPasswordModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
populate: ['user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing app passwords.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of app passwords (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listAppPasswordsByPropertiesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
properties = '',
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {}
|
||||||
|
) => {
|
||||||
|
const result = await listObjectsByProperties({
|
||||||
|
model: appPasswordModel,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
masterFilter,
|
||||||
|
populate: ['user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing app passwords.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.forEach((item) => {
|
||||||
|
item.secret = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`List of app passwords. Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppPasswordRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: appPasswordModel,
|
||||||
|
id,
|
||||||
|
populate: ['user'],
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
logger.warn(`App password not found with supplied id.`);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.debug(`Retrieved app password with ID: ${id}`);
|
||||||
|
res.send({ ...result, secret: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editAppPasswordRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
logger.trace(`App password with ID: ${id}`);
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: req.body.name,
|
||||||
|
active: req.body.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.body.secret) {
|
||||||
|
updateData.secret = await bcrypt.hash(req.body.secret, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: appPasswordModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error editing app password:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited app password with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newAppPasswordRouteHandler = async (req, res) => {
|
||||||
|
const plainSecret = req.body.secret?.trim() || nanoid(32);
|
||||||
|
const secretHash = await bcrypt.hash(plainSecret, 10);
|
||||||
|
|
||||||
|
const newData = {
|
||||||
|
name: req.body.name,
|
||||||
|
user: req.body.user,
|
||||||
|
active: req.body.active ?? true,
|
||||||
|
secret: secretHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await newObject({
|
||||||
|
model: appPasswordModel,
|
||||||
|
newData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('No app password created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`New app password with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAppPasswordRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
logger.trace(`App password with ID: ${id}`);
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: appPasswordModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('No app password deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Deleted app password with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppPasswordStatsRouteHandler = async (req, res) => {
|
||||||
|
const result = await getModelStats({ model: appPasswordModel });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching app password stats:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('App password stats:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppPasswordHistoryRouteHandler = async (req, res) => {
|
||||||
|
const from = req.query.from;
|
||||||
|
const to = req.query.to;
|
||||||
|
const result = await getModelHistory({ model: appPasswordModel, from, to });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching app password history:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('App password history:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const regenerateSecretRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
const appPasswordDoc = await appPasswordModel.findById(id).select('+secret').populate('user');
|
||||||
|
if (!appPasswordDoc) {
|
||||||
|
return res.status(404).send({ error: 'App password not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPasswordUserId =
|
||||||
|
appPasswordDoc.user?._id?.toString?.() ?? appPasswordDoc.user?.toString?.();
|
||||||
|
if (appPasswordUserId !== req.user._id.toString()) {
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'You are not authorized to regenerate the secret for this app password.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainSecret = nanoid(32);
|
||||||
|
const secretHash = await bcrypt.hash(plainSecret, 10);
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: appPasswordModel,
|
||||||
|
id,
|
||||||
|
updateData: { secret: secretHash, updatedAt: new Date() },
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error regenerating app password secret:', result.error);
|
||||||
|
return res.status(result.code || 500).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Regenerated secret for app password with ID: ${id}`);
|
||||||
|
res.send({ appPassword: plainSecret });
|
||||||
|
};
|
||||||
@ -8,7 +8,7 @@ import { getFilter } from '../../utils.js';
|
|||||||
const logger = log4js.getLogger('OData');
|
const logger = log4js.getLogger('OData');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
const EXCLUDED_PATHS = ['appPasswordHash', '__v'];
|
const EXCLUDED_PATHS = ['appPasswordHash', 'secret', '__v'];
|
||||||
|
|
||||||
function mongooseTypeToEdm(path) {
|
function mongooseTypeToEdm(path) {
|
||||||
const instance = path?.instance;
|
const instance = path?.instance;
|
||||||
@ -354,6 +354,7 @@ function getModelFilterFields(objectType) {
|
|||||||
salesOrder: ['client'],
|
salesOrder: ['client'],
|
||||||
invoice: ['to._id', 'from._id', 'order._id', 'orderType'],
|
invoice: ['to._id', 'from._id', 'order._id', 'orderType'],
|
||||||
auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
||||||
|
appPassword: ['name', 'user', 'active'],
|
||||||
};
|
};
|
||||||
const extra = byType[objectType] || [];
|
const extra = byType[objectType] || [];
|
||||||
return [...base, ...extra];
|
return [...base, ...extra];
|
||||||
|
|||||||
32
src/utils.js
32
src/utils.js
@ -361,12 +361,24 @@ function getChangedValues(oldObj, newObj, old = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AUDIT_EXCLUDED_MODELS = ['notification', 'userNotifier'];
|
const AUDIT_EXCLUDED_MODELS = ['notification', 'userNotifier'];
|
||||||
|
const SENSITIVE_KEYS = ['secret'];
|
||||||
|
|
||||||
|
function omitSensitive(obj) {
|
||||||
|
if (obj == null || typeof obj !== 'object') return obj;
|
||||||
|
if (Array.isArray(obj)) return obj.map(omitSensitive);
|
||||||
|
const result = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (SENSITIVE_KEYS.includes(key)) continue;
|
||||||
|
result[key] = omitSensitive(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async function newAuditLog(newValue, parentId, parentType, user) {
|
async function newAuditLog(newValue, parentId, parentType, user) {
|
||||||
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||||
|
|
||||||
// Filter out createdAt and updatedAt from newValue
|
// Filter out createdAt, updatedAt, and sensitive fields from newValue
|
||||||
const filteredNewValue = { ...newValue };
|
const filteredNewValue = omitSensitive({ ...newValue });
|
||||||
delete filteredNewValue.createdAt;
|
delete filteredNewValue.createdAt;
|
||||||
delete filteredNewValue.updatedAt;
|
delete filteredNewValue.updatedAt;
|
||||||
const auditLog = new auditLogModel({
|
const auditLog = new auditLogModel({
|
||||||
@ -389,8 +401,8 @@ async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
|||||||
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||||
|
|
||||||
// Get only the changed values
|
// Get only the changed values
|
||||||
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
const changedOldValues = omitSensitive(getChangedValues(oldValue, newValue, true));
|
||||||
const changedNewValues = getChangedValues(oldValue, newValue, false);
|
const changedNewValues = omitSensitive(getChangedValues(oldValue, newValue, false));
|
||||||
|
|
||||||
// If no values changed, don't create an audit log
|
// If no values changed, don't create an audit log
|
||||||
if (Object.keys(changedOldValues).length === 0 || Object.keys(changedNewValues).length === 0) {
|
if (Object.keys(changedOldValues).length === 0 || Object.keys(changedNewValues).length === 0) {
|
||||||
@ -417,8 +429,8 @@ async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
|||||||
async function editNotification(oldValue, newValue, parentId, parentType, user) {
|
async function editNotification(oldValue, newValue, parentId, parentType, user) {
|
||||||
const model = getModelByName(parentType);
|
const model = getModelByName(parentType);
|
||||||
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||||
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
const changedOldValues = omitSensitive(getChangedValues(oldValue, newValue, true));
|
||||||
const changedNewValues = getChangedValues(oldValue, newValue, false);
|
const changedNewValues = omitSensitive(getChangedValues(oldValue, newValue, false));
|
||||||
|
|
||||||
if (Object.keys(changedOldValues).length === 0 || Object.keys(changedNewValues).length === 0) {
|
if (Object.keys(changedOldValues).length === 0 || Object.keys(changedNewValues).length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -449,7 +461,7 @@ async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
|||||||
|
|
||||||
const auditLog = new auditLogModel({
|
const auditLog = new auditLogModel({
|
||||||
changes: {
|
changes: {
|
||||||
old: deleteValue,
|
old: omitSensitive(deleteValue),
|
||||||
},
|
},
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
parentType,
|
parentType,
|
||||||
@ -473,7 +485,7 @@ async function deleteNotification(object, parentId, parentType, user) {
|
|||||||
`The ${parentType} ${parentId} has been deleted.`,
|
`The ${parentType} ${parentId} has been deleted.`,
|
||||||
'deleteObject',
|
'deleteObject',
|
||||||
{
|
{
|
||||||
object: object,
|
object: omitSensitive(object),
|
||||||
objectType: parentType,
|
objectType: parentType,
|
||||||
object: { _id: parentId },
|
object: { _id: parentId },
|
||||||
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||||
@ -599,7 +611,7 @@ async function createNotification(user, title, message, type = 'info', metadata)
|
|||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
metadata,
|
metadata: omitSensitive(metadata ?? {}),
|
||||||
});
|
});
|
||||||
await notification.save();
|
await notification.save();
|
||||||
const value = notification.toJSON ? notification.toJSON() : notification;
|
const value = notification.toJSON ? notification.toJSON() : notification;
|
||||||
@ -658,7 +670,7 @@ async function sendEmailNotification(user, title, message, type = 'info', metada
|
|||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
metadata: metadata || {},
|
metadata: omitSensitive(metadata || {}),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
authCode,
|
authCode,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user