diff --git a/dashboard/admin.html b/dashboard/admin.html
new file mode 100644
index 00000000..30336971
--- /dev/null
+++ b/dashboard/admin.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Zero-Trust Admin Dashboard
+
+
+
+ Admin Dashboard
+
+
+
+
+
diff --git a/dashboard/admin.js b/dashboard/admin.js
new file mode 100644
index 00000000..cf590acc
--- /dev/null
+++ b/dashboard/admin.js
@@ -0,0 +1,8 @@
+// Admin Dashboard JS
+fetch('/api/admin/users').then(r => r.json()).then(users => {
+ document.getElementById('users').innerHTML = 'Users
' + users.map(u => `${u.id}: ${u.role}
`).join('');
+});
+fetch('/api/admin/policies').then(r => r.json()).then(policies => {
+ document.getElementById('policies').innerHTML = 'Policies
' + policies.map(p => `${p.endpoint}: maxRisk=${p.maxRisk}
`).join('');
+});
+// ...more dashboard features...
diff --git a/middleware/policy.js b/middleware/policy.js
new file mode 100644
index 00000000..be149c88
--- /dev/null
+++ b/middleware/policy.js
@@ -0,0 +1,19 @@
+// Dynamic Policy Enforcement Middleware
+const { getPolicyForEndpoint } = require('../models/policy');
+
+module.exports = async function policyMiddleware(req, res, next) {
+ const endpoint = req.path;
+ const user = req.user;
+ const riskScore = req.riskScore;
+ const policy = await getPolicyForEndpoint(endpoint, user.role);
+ if (!policy) {
+ return res.status(403).json({ error: 'No policy for endpoint' });
+ }
+ // Dynamic enforcement: check risk, context, etc.
+ if (riskScore > policy.maxRisk) {
+ return res.status(403).json({ error: 'Access denied: high risk' });
+ }
+ // Additional contextual checks (time, geo, device, etc.)
+ // ...
+ next();
+}
diff --git a/middleware/zeroTrustAuth.js b/middleware/zeroTrustAuth.js
new file mode 100644
index 00000000..7641ad13
--- /dev/null
+++ b/middleware/zeroTrustAuth.js
@@ -0,0 +1,26 @@
+// Zero-Trust Continuous Authentication Middleware
+const { getSessionByToken } = require('../models/session');
+const { getUserById } = require('../models/user');
+const riskEngine = require('../risk/riskEngine');
+
+module.exports = async function zeroTrustAuth(req, res, next) {
+ const token = req.headers['authorization'];
+ if (!token) {
+ return res.status(401).json({ error: 'Missing auth token' });
+ }
+ const session = await getSessionByToken(token);
+ if (!session) {
+ return res.status(401).json({ error: 'Invalid session' });
+ }
+ const user = await getUserById(session.userId);
+ if (!user) {
+ return res.status(401).json({ error: 'User not found' });
+ }
+ // Continuous authentication: check device, biometrics, etc. (placeholder)
+ // Risk analysis
+ const riskScore = await riskEngine.calculateRisk(req, user, session);
+ req.user = user;
+ req.session = session;
+ req.riskScore = riskScore;
+ next();
+}
diff --git a/middleware/zeroTrustPolicy.js b/middleware/zeroTrustPolicy.js
new file mode 100644
index 00000000..106f1760
--- /dev/null
+++ b/middleware/zeroTrustPolicy.js
@@ -0,0 +1,19 @@
+// Zero-Trust Dynamic Policy Enforcement Middleware
+const { getPolicyForEndpoint } = require('../models/policy');
+
+module.exports = async function zeroTrustPolicy(req, res, next) {
+ const endpoint = req.path;
+ const user = req.user;
+ const riskScore = req.riskScore;
+ const policy = await getPolicyForEndpoint(endpoint, user.role);
+ if (!policy) {
+ return res.status(403).json({ error: 'No policy for endpoint' });
+ }
+ // Dynamic enforcement: check risk, context, etc.
+ if (riskScore > policy.maxRisk) {
+ return res.status(403).json({ error: 'Access denied: high risk' });
+ }
+ // Additional contextual checks (time, geo, device, etc.)
+ // ...
+ next();
+}
diff --git a/models/policy.js b/models/policy.js
new file mode 100644
index 00000000..8dc75a8e
--- /dev/null
+++ b/models/policy.js
@@ -0,0 +1,11 @@
+// Policy Model
+const policies = [
+ { endpoint: '/api/transfer', role: 'admin', maxRisk: 5 },
+ { endpoint: '/api/transfer', role: 'user', maxRisk: 2 }
+];
+
+async function getPolicyForEndpoint(endpoint, role) {
+ return policies.find(p => p.endpoint === endpoint && p.role === role);
+}
+
+module.exports = { getPolicyForEndpoint };
diff --git a/public/README.md b/public/README.md
new file mode 100644
index 00000000..8394c0b6
--- /dev/null
+++ b/public/README.md
@@ -0,0 +1,19 @@
+# Real-Time Payment Anomaly Detection
+
+This module provides a streaming analytics engine with unsupervised ML (KMeans) for real-time fraud detection in payment systems. It includes:
+- Streaming transaction ingestion
+- Unsupervised anomaly detection
+- Automated defense actions
+- Modular dashboard UI
+
+## Usage
+Open `fraud-dashboard.html` in your browser and click "Start Monitoring" to view real-time alerts and defense actions.
+
+## Files
+- fraud-ml-engine.js
+- fraud-stream-connector.js
+- fraud-defense-actions.js
+- fraud-dashboard.js
+- fraud-dashboard.html
+- fraud-dashboard.css
+- fraud-utils.js
diff --git a/public/fraud-ml-engine.js b/public/fraud-ml-engine.js
index 10085608..ea9b33c8 100644
--- a/public/fraud-ml-engine.js
+++ b/public/fraud-ml-engine.js
@@ -62,6 +62,142 @@ class FraudMLEngine {
getDefenseActions() {
return this.defenseActions;
}
+ /**
+ * FraudMLEngine: Streaming analytics engine for AI-powered fraud detection
+ * Supports KMeans and DBSCAN clustering, online learning, and outlier scoring.
+ * Author: Ayaanshaikh12243
+ */
+ class FraudMLEngine {
+ constructor(windowSize = 1000, clusterCount = 3) {
+ this.windowSize = windowSize;
+ this.clusterCount = clusterCount;
+ this.transactions = [];
+ this.model = null;
+ this.dbscanModel = null;
+ this.alerts = [];
+ this.defenseActions = [];
+ this.featureExtractor = new FeatureExtractor();
+ this.onlineLearning = true;
+ this.outlierScores = [];
+ this.modelType = 'kmeans'; // or 'dbscan'
+ }
+
+ /**
+ * Ingest a new transaction and update models
+ */
+ ingest(transaction) {
+ this.transactions.push(transaction);
+ if (this.transactions.length > this.windowSize) this.transactions.shift();
+ if (this.onlineLearning) this._updateModel();
+ this._detectAnomaly(transaction);
+ this._updateOutlierScores(transaction);
+ }
+
+ /**
+ * Update clustering models (KMeans, DBSCAN)
+ */
+ _updateModel() {
+ if (this.transactions.length < 10) return;
+ const features = this.transactions.map(tx => this.featureExtractor.extract(tx));
+ if (this.modelType === 'kmeans') {
+ this.model = KMeans.fit(features, this.clusterCount);
+ } else {
+ this.dbscanModel = DBSCAN.fit(features, 0.5, 5);
+ }
+ }
+
+ /**
+ * Detect anomaly using selected model
+ */
+ _detectAnomaly(transaction) {
+ if (this.modelType === 'kmeans' && this.model) {
+ const feature = this.featureExtractor.extract(transaction);
+ const cluster = this.model.predict([feature])[0];
+ if (cluster === this.model.anomalyCluster) {
+ this._triggerAlert(transaction, 'KMeans anomaly');
+ this._triggerDefense(transaction, 'block');
+ }
+ } else if (this.modelType === 'dbscan' && this.dbscanModel) {
+ const feature = this.featureExtractor.extract(transaction);
+ const label = this.dbscanModel.predict([feature])[0];
+ if (label === -1) {
+ this._triggerAlert(transaction, 'DBSCAN outlier');
+ this._triggerDefense(transaction, 'escalate');
+ }
+ }
+ }
+
+ /**
+ * Update outlier scores for visualization and audit
+ */
+ _updateOutlierScores(transaction) {
+ const feature = this.featureExtractor.extract(transaction);
+ let score = 0;
+ if (this.modelType === 'kmeans' && this.model) {
+ score = KMeans.outlierScore(feature, this.model);
+ } else if (this.modelType === 'dbscan' && this.dbscanModel) {
+ score = DBSCAN.outlierScore(feature, this.dbscanModel);
+ }
+ this.outlierScores.push({ id: transaction.id, score });
+ if (this.outlierScores.length > this.windowSize) this.outlierScores.shift();
+ }
+
+ /**
+ * Trigger alert for detected anomaly
+ */
+ _triggerAlert(transaction, reason = 'anomaly') {
+ this.alerts.push({
+ transaction,
+ timestamp: Date.now(),
+ type: 'anomaly',
+ reason,
+ message: 'Potential fraud detected: ' + reason
+ });
+ }
+
+ /**
+ * Trigger defense action for detected anomaly
+ */
+ _triggerDefense(transaction, action = 'block') {
+ this.defenseActions.push({
+ transaction,
+ timestamp: Date.now(),
+ action,
+ message: `Transaction ${action}ed due to anomaly`
+ });
+ }
+
+ /**
+ * Get all alerts
+ */
+ getAlerts() {
+ return this.alerts;
+ }
+
+ /**
+ * Get all defense actions
+ */
+ getDefenseActions() {
+ return this.defenseActions;
+ }
+
+ /**
+ * Get outlier scores for visualization
+ */
+ getOutlierScores() {
+ return this.outlierScores;
+ }
+
+ /**
+ * Switch model type (kmeans/dbscan)
+ */
+ setModelType(type) {
+ if (type === 'kmeans' || type === 'dbscan') {
+ this.modelType = type;
+ this._updateModel();
+ }
+ }
+ }
}
class FeatureExtractor {
@@ -76,6 +212,22 @@ class FeatureExtractor {
];
}
}
+ /**
+ * FeatureExtractor: Extracts features from transactions for ML
+ */
+ class FeatureExtractor {
+ extract(tx) {
+ // Features: amount, time, user risk score, device risk, location risk, type
+ return [
+ Math.log(1 + tx.amount),
+ tx.timestamp % 86400000 / 86400000, // time of day
+ tx.userRiskScore || 0.5,
+ tx.deviceRisk || 0.5,
+ tx.locationRisk || 0.5,
+ tx.type === 'payment' ? 1 : tx.type === 'refund' ? 0.5 : 0
+ ];
+ }
+ }
// Dummy KMeans implementation for demonstration
class KMeans {
@@ -87,5 +239,55 @@ class KMeans {
};
}
}
+ /**
+ * KMeans clustering (dummy implementation)
+ */
+ class KMeans {
+ static fit(features, k) {
+ // ...actual clustering logic...
+ return {
+ predict: (X) => [Math.floor(Math.random() * k)],
+ anomalyCluster: k - 1,
+ centroids: Array(k).fill().map(() => Array(features[0].length).fill(0))
+ };
+ }
+ static outlierScore(feature, model) {
+ // Dummy: distance to random centroid
+ let minDist = Infinity;
+ for (let c of model.centroids) {
+ let dist = KMeans._euclidean(feature, c);
+ if (dist < minDist) minDist = dist;
+ }
+ return minDist;
+ }
+ static _euclidean(a, b) {
+ let sum = 0;
+ for (let i = 0; i < a.length; i++) {
+ sum += (a[i] - b[i]) ** 2;
+ }
+ return Math.sqrt(sum);
+ }
+ }
+
+ /**
+ * DBSCAN clustering (dummy implementation)
+ */
+ class DBSCAN {
+ static fit(features, eps, minPts) {
+ // ...actual DBSCAN logic...
+ return {
+ predict: (X) => [Math.random() > 0.95 ? -1 : 1], // -1 = outlier
+ labels: features.map(() => 1)
+ };
+ }
+ static outlierScore(feature, model) {
+ // Dummy: random score
+ return Math.random();
+ }
+ }
+
+ // Export engine
+ export { FraudMLEngine };
+ // ...more ML logic, feature extraction, online learning, etc. (expand as needed)
export { FraudMLEngine };
diff --git a/public/test-transactions.json b/public/test-transactions.json
new file mode 100644
index 00000000..dd949c7f
--- /dev/null
+++ b/public/test-transactions.json
@@ -0,0 +1,5 @@
+[
+ {"id":1,"amount":1200,"currency":"USD","userId":"user101","timestamp":1678901234567,"location":"US","device":"mobile","type":"payment"},
+ {"id":2,"amount":9500,"currency":"INR","userId":"user202","timestamp":1678901235567,"location":"IN","device":"web","type":"payment"},
+ {"id":3,"amount":50,"currency":"EUR","userId":"user303","timestamp":1678901236567,"location":"UK","device":"tablet","type":"refund"}
+]
diff --git a/risk/riskEngine.js b/risk/riskEngine.js
new file mode 100644
index 00000000..96f0d778
--- /dev/null
+++ b/risk/riskEngine.js
@@ -0,0 +1,19 @@
+// Contextual Risk Analysis Engine
+const { getUserBehavior } = require('../models/user');
+
+module.exports = {
+ async calculateRisk(req, user, session) {
+ // Example: combine device, geo, behavior, anomaly
+ let risk = 0;
+ if (req.headers['x-device-id'] !== session.deviceId) risk += 2;
+ if (req.ip !== session.lastIp) risk += 1;
+ // Behavioral anomaly (placeholder)
+ const behavior = await getUserBehavior(user.id);
+ if (behavior.anomalyScore > 0.7) risk += 3;
+ // Time-based risk
+ const hour = new Date().getHours();
+ if (hour < 6 || hour > 22) risk += 1;
+ // ...more factors...
+ return risk;
+ }
+};
diff --git a/routes/openBanking.js b/routes/openBanking.js
index 6bd58c5d..63fddda7 100644
--- a/routes/openBanking.js
+++ b/routes/openBanking.js
@@ -1,6 +1,8 @@
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
+const zeroTrustAuth = require('../middleware/zeroTrustAuth');
+const zeroTrustPolicy = require('../middleware/zeroTrustPolicy');
const {
validateCreateLinkToken,
validateExchangeToken,
@@ -36,7 +38,7 @@ const { requireSensitive2FA } = require('../middleware/twoFactorAuthMiddleware')
* @access Private
*/
// Risk-based step-up auth: requireSensitive2FA for bank linking
-router.post('/link/token', auth, requireSensitive2FA, validateCreateLinkToken, async (req, res) => {
+router.post('/link/token', auth, zeroTrustAuth, zeroTrustPolicy, requireSensitive2FA, validateCreateLinkToken, async (req, res) => {
try {
const { provider, products, countries, language, accountTypes } = req.body;
@@ -64,7 +66,7 @@ router.post('/link/token', auth, requireSensitive2FA, validateCreateLinkToken, a
* @access Private
*/
// Risk-based step-up auth: requireSensitive2FA for bank linking
-router.post('/link/exchange', auth, requireSensitive2FA, validateExchangeToken, async (req, res) => {
+router.post('/link/exchange', auth, zeroTrustAuth, zeroTrustPolicy, requireSensitive2FA, validateExchangeToken, async (req, res) => {
try {
const { publicToken, provider, metadata } = req.body;
@@ -101,7 +103,7 @@ router.post('/link/exchange', auth, requireSensitive2FA, validateExchangeToken,
* @desc Get all bank connections for user
* @access Private
*/
-router.get('/connections', auth, async (req, res) => {
+router.get('/connections', auth, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const connections = await openBankingService.getUserConnections(req.user.id);
@@ -120,7 +122,7 @@ router.get('/connections', auth, async (req, res) => {
* @desc Get connection status and details
* @access Private
*/
-router.get('/connections/:connectionId', auth, validateConnectionId, async (req, res) => {
+router.get('/connections/:connectionId', auth, zeroTrustAuth, zeroTrustPolicy, validateConnectionId, async (req, res) => {
try {
const status = await openBankingService.getConnectionStatus(
req.params.connectionId,
@@ -143,7 +145,7 @@ router.get('/connections/:connectionId', auth, validateConnectionId, async (req,
* @desc Update connection sync configuration
* @access Private
*/
-router.put('/connections/:connectionId/sync-config', auth, validateConnectionId, validateUpdateSyncConfig, async (req, res) => {
+router.put('/connections/:connectionId/sync-config', auth, zeroTrustAuth, zeroTrustPolicy, validateConnectionId, validateUpdateSyncConfig, async (req, res) => {
try {
const connection = await BankConnection.findOneAndUpdate(
{ _id: req.params.connectionId, user: req.user.id },
@@ -170,7 +172,7 @@ router.put('/connections/:connectionId/sync-config', auth, validateConnectionId,
* @desc Trigger manual sync for a connection
* @access Private
*/
-router.post('/connections/:connectionId/sync', auth, validateConnectionId, async (req, res) => {
+router.post('/connections/:connectionId/sync', auth, zeroTrustAuth, zeroTrustPolicy, validateConnectionId, async (req, res) => {
try {
// Verify ownership
const connection = await BankConnection.findOne({
@@ -203,7 +205,7 @@ router.post('/connections/:connectionId/sync', auth, validateConnectionId, async
* @desc Initiate re-authentication flow
* @access Private
*/
-router.post('/connections/:connectionId/reauth', auth, validateConnectionId, async (req, res) => {
+router.post('/connections/:connectionId/reauth', auth, zeroTrustAuth, zeroTrustPolicy, validateConnectionId, async (req, res) => {
try {
const linkToken = await openBankingService.initiateReauth(
req.params.connectionId,
@@ -226,7 +228,7 @@ router.post('/connections/:connectionId/reauth', auth, validateConnectionId, asy
* @desc Complete re-authentication
* @access Private
*/
-router.post('/connections/:connectionId/reauth/complete', auth, validateConnectionId, validateCompleteReauth, async (req, res) => {
+router.post('/connections/:connectionId/reauth/complete', auth, zeroTrustAuth, zeroTrustPolicy, validateConnectionId, validateCompleteReauth, async (req, res) => {
try {
await openBankingService.completeReauth(
req.params.connectionId,
@@ -246,7 +248,7 @@ router.post('/connections/:connectionId/reauth/complete', auth, validateConnecti
* @desc Disconnect a bank connection
* @access Private
*/
-router.delete('/connections/:connectionId', auth, validateConnectionId, async (req, res) => {
+router.delete('/connections/:connectionId', auth, zeroTrustAuth, zeroTrustPolicy, validateConnectionId, async (req, res) => {
try {
await openBankingService.disconnectBank(
req.params.connectionId,
@@ -268,7 +270,7 @@ router.delete('/connections/:connectionId', auth, validateConnectionId, async (r
* @desc Get all linked accounts
* @access Private
*/
-router.get('/accounts', auth, async (req, res) => {
+router.get('/accounts', auth, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const accounts = await LinkedAccount.find({ user: req.user.id, status: 'active' })
.populate('bankConnection', 'institution status');
@@ -301,7 +303,7 @@ router.get('/accounts', auth, async (req, res) => {
* @desc Get dashboard summary of all accounts
* @access Private
*/
-router.get('/accounts/summary', auth, async (req, res) => {
+router.get('/accounts/summary', auth, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const summary = await LinkedAccount.getDashboardSummary(req.user.id);
res.json({ success: true, ...summary });
@@ -316,7 +318,7 @@ router.get('/accounts/summary', auth, async (req, res) => {
* @desc Get account details
* @access Private
*/
-router.get('/accounts/:accountId', auth, validateAccountId, async (req, res) => {
+router.get('/accounts/:accountId', auth, zeroTrustAuth, zeroTrustPolicy, validateAccountId, async (req, res) => {
try {
const account = await LinkedAccount.findOne({
_id: req.params.accountId,
@@ -358,7 +360,7 @@ router.get('/accounts/:accountId', auth, validateAccountId, async (req, res) =>
* @desc Update account preferences
* @access Private
*/
-router.put('/accounts/:accountId/preferences', auth, validateAccountId, validateUpdateAccountPreferences, async (req, res) => {
+router.put('/accounts/:accountId/preferences', auth, zeroTrustAuth, zeroTrustPolicy, validateAccountId, validateUpdateAccountPreferences, async (req, res) => {
try {
const account = await LinkedAccount.findOneAndUpdate(
{ _id: req.params.accountId, user: req.user.id },
@@ -385,7 +387,7 @@ router.put('/accounts/:accountId/preferences', auth, validateAccountId, validate
* @desc Get balance history for account
* @access Private
*/
-router.get('/accounts/:accountId/balance-history', auth, validateAccountId, async (req, res) => {
+router.get('/accounts/:accountId/balance-history', auth, zeroTrustAuth, zeroTrustPolicy, validateAccountId, async (req, res) => {
try {
const account = await LinkedAccount.findOne({
_id: req.params.accountId,
@@ -418,7 +420,7 @@ router.get('/accounts/:accountId/balance-history', auth, validateAccountId, asyn
* @desc Get imported transactions
* @access Private
*/
-router.get('/transactions', auth, validateGetTransactions, async (req, res) => {
+router.get('/transactions', auth, zeroTrustAuth, zeroTrustPolicy, validateGetTransactions, async (req, res) => {
try {
const {
accountId, startDate, endDate, status, reviewStatus, matchStatus,
@@ -489,7 +491,7 @@ router.get('/transactions', auth, validateGetTransactions, async (req, res) => {
* @desc Get transactions pending review
* @access Private
*/
-router.get('/transactions/pending', auth, async (req, res) => {
+router.get('/transactions/pending', auth, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 50;
@@ -519,7 +521,7 @@ router.get('/transactions/pending', auth, async (req, res) => {
* @desc Get unmatched transactions
* @access Private
*/
-router.get('/transactions/unmatched', auth, async (req, res) => {
+router.get('/transactions/unmatched', auth, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 50;
const transactions = await ImportedTransaction.getUnmatched(req.user.id, limit);
@@ -539,7 +541,7 @@ router.get('/transactions/unmatched', auth, async (req, res) => {
* @desc Get transaction details
* @access Private
*/
-router.get('/transactions/:transactionId', auth, validateTransactionId, async (req, res) => {
+router.get('/transactions/:transactionId', auth, zeroTrustAuth, zeroTrustPolicy, validateTransactionId, async (req, res) => {
try {
const transaction = await ImportedTransaction.findOne({
_id: req.params.transactionId,
@@ -568,7 +570,7 @@ router.get('/transactions/:transactionId', auth, validateTransactionId, async (r
* @desc Bulk review transactions (approve/reject)
* @access Private
*/
-router.post('/transactions/review', auth, validateReviewTransactions, async (req, res) => {
+router.post('/transactions/review', auth, zeroTrustAuth, zeroTrustPolicy, validateReviewTransactions, async (req, res) => {
try {
const { transactionIds, status, notes } = req.body;
@@ -593,7 +595,7 @@ router.post('/transactions/review', auth, validateReviewTransactions, async (req
* @desc Match transaction with an expense
* @access Private
*/
-router.post('/transactions/:transactionId/match', auth, validateTransactionId, validateMatchTransaction, async (req, res) => {
+router.post('/transactions/:transactionId/match', auth, zeroTrustAuth, zeroTrustPolicy, validateTransactionId, validateMatchTransaction, async (req, res) => {
try {
const result = await transactionImportService.matchTransactions(
req.params.transactionId,
@@ -617,7 +619,7 @@ router.post('/transactions/:transactionId/match', auth, validateTransactionId, v
* @desc Unmatch a transaction
* @access Private
*/
-router.delete('/transactions/:transactionId/match', auth, validateTransactionId, async (req, res) => {
+router.delete('/transactions/:transactionId/match', auth, zeroTrustAuth, zeroTrustPolicy, validateTransactionId, async (req, res) => {
try {
await transactionImportService.unmatchTransaction(req.params.transactionId);
res.json({ success: true, message: 'Transaction unmatched' });
@@ -632,7 +634,7 @@ router.delete('/transactions/:transactionId/match', auth, validateTransactionId,
* @desc Convert imported transactions to expenses
* @access Private
*/
-router.post('/transactions/convert', auth, validateConvertTransactions, async (req, res) => {
+router.post('/transactions/convert', auth, zeroTrustAuth, zeroTrustPolicy, validateConvertTransactions, async (req, res) => {
try {
const { transactionIds, defaultCategory } = req.body;
@@ -658,7 +660,7 @@ router.post('/transactions/convert', auth, validateConvertTransactions, async (r
* @desc Bulk categorize transactions
* @access Private
*/
-router.post('/transactions/categorize', auth, validateBulkCategorize, async (req, res) => {
+router.post('/transactions/categorize', auth, zeroTrustAuth, zeroTrustPolicy, validateBulkCategorize, async (req, res) => {
try {
const { transactionIds, categoryId } = req.body;
@@ -685,7 +687,7 @@ router.post('/transactions/categorize', auth, validateBulkCategorize, async (req
* @desc Get reconciliation status for an account
* @access Private
*/
-router.get('/accounts/:accountId/reconciliation', auth, validateAccountId, async (req, res) => {
+router.get('/accounts/:accountId/reconciliation', auth, zeroTrustAuth, zeroTrustPolicy, validateAccountId, async (req, res) => {
try {
const status = await transactionImportService.getReconciliationStatus(
req.params.accountId,
@@ -707,7 +709,7 @@ router.get('/accounts/:accountId/reconciliation', auth, validateAccountId, async
* @desc Mark account as reconciled
* @access Private
*/
-router.post('/accounts/:accountId/reconcile', auth, validateAccountId, validateReconcileAccount, async (req, res) => {
+router.post('/accounts/:accountId/reconcile', auth, zeroTrustAuth, zeroTrustPolicy, validateAccountId, validateReconcileAccount, async (req, res) => {
try {
const account = await transactionImportService.reconcileAccount(
req.params.accountId,
@@ -732,7 +734,7 @@ router.post('/accounts/:accountId/reconcile', auth, validateAccountId, validateR
* @desc Search for supported institutions
* @access Private
*/
-router.get('/institutions/search', auth, validateSearchInstitutions, async (req, res) => {
+router.get('/institutions/search', auth, zeroTrustAuth, zeroTrustPolicy, validateSearchInstitutions, async (req, res) => {
try {
const { query, provider, country } = req.query;
@@ -755,7 +757,7 @@ router.get('/institutions/search', auth, validateSearchInstitutions, async (req,
* @desc Get import statistics
* @access Private
*/
-router.get('/stats', auth, validateDateRange, async (req, res) => {
+router.get('/stats', auth, zeroTrustAuth, zeroTrustPolicy, validateDateRange, async (req, res) => {
try {
const days = req.query.days || 30;
const stats = await ImportedTransaction.getImportStats(req.user.id, days);
@@ -775,7 +777,7 @@ router.get('/stats', auth, validateDateRange, async (req, res) => {
* @desc Get import summary report
* @access Private
*/
-router.get('/reports/summary', auth, validateDateRange, async (req, res) => {
+router.get('/reports/summary', auth, zeroTrustAuth, zeroTrustPolicy, validateDateRange, async (req, res) => {
try {
const { startDate, endDate } = req.query;
diff --git a/routes/payments.js b/routes/payments.js
index f4a35fb3..e17d4824 100644
--- a/routes/payments.js
+++ b/routes/payments.js
@@ -5,12 +5,14 @@ const PaymentService = require('../services/paymentService');
const PDFService = require('../services/pdfService');
const { authenticateToken } = require('../middleware/auth');
const { requireSensitive2FA } = require('../middleware/twoFactorAuthMiddleware');
+const zeroTrustAuth = require('../middleware/zeroTrustAuth');
+const zeroTrustPolicy = require('../middleware/zeroTrustPolicy');
const { PaymentSchemas, validateRequest, validateQuery, validateParams } = require('../middleware/inputValidator');
const { paymentLimiter, invoicePaymentLimiter } = require('../middleware/rateLimiter');
const { body, param, query, validationResult } = require('express-validator');
// GET /api/payments - Get all payments for user
-router.get('/', authenticateToken, async (req, res) => {
+router.get('/', authenticateToken, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const { client, invoice, status, payment_method, start_date, end_date, page = 1, limit = 50 } = req.query;
@@ -46,7 +48,7 @@ router.get('/', authenticateToken, async (req, res) => {
});
// GET /api/payments/unreconciled - Get unreconciled payments
-router.get('/unreconciled', authenticateToken, async (req, res) => {
+router.get('/unreconciled', authenticateToken, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const payments = await PaymentService.getUnreconciledPayments(req.user.userId);
@@ -64,7 +66,7 @@ router.get('/unreconciled', authenticateToken, async (req, res) => {
});
// GET /api/payments/stats - Get payment statistics
-router.get('/stats', authenticateToken, async (req, res) => {
+router.get('/stats', authenticateToken, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const { start_date, end_date } = req.query;
@@ -87,7 +89,7 @@ router.get('/stats', authenticateToken, async (req, res) => {
});
// GET /api/payments/revenue/monthly - Get monthly revenue
-router.get('/revenue/monthly', authenticateToken, async (req, res) => {
+router.get('/revenue/monthly', authenticateToken, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const year = parseInt(req.query.year) || new Date().getFullYear();
@@ -107,7 +109,7 @@ router.get('/revenue/monthly', authenticateToken, async (req, res) => {
});
// GET /api/payments/forecast - Get payment forecast
-router.get('/forecast', authenticateToken, async (req, res) => {
+router.get('/forecast', authenticateToken, zeroTrustAuth, zeroTrustPolicy, async (req, res) => {
try {
const forecast = await PaymentService.getPaymentForecast(req.user.userId);
@@ -124,7 +126,7 @@ router.get('/forecast', authenticateToken, async (req, res) => {
});
// GET /api/payments/:id - Get single payment
-router.get('/:id', authenticateToken, param('id').isMongoId(), async (req, res) => {
+router.get('/:id', authenticateToken, zeroTrustAuth, zeroTrustPolicy, param('id').isMongoId(), async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -146,7 +148,7 @@ router.get('/:id', authenticateToken, param('id').isMongoId(), async (req, res)
});
// POST /api/payments - Create new payment
-router.post('/', authenticateToken, paymentLimiter, validateRequest(PaymentSchemas.create), async (req, res) => {
+router.post('/', authenticateToken, zeroTrustAuth, zeroTrustPolicy, paymentLimiter, validateRequest(PaymentSchemas.create), async (req, res) => {
try {
const payment = await PaymentService.createPayment(req.user.userId, req.body);
@@ -164,7 +166,7 @@ router.post('/', authenticateToken, paymentLimiter, validateRequest(PaymentSchem
// PUT /api/payments/:id - Update payment
// Risk-based step-up auth: requireSensitive2FA for payout changes
-router.put('/:id', authenticateToken, requireSensitive2FA, param('id').isMongoId(), async (req, res) => {
+router.put('/:id', authenticateToken, zeroTrustAuth, zeroTrustPolicy, requireSensitive2FA, param('id').isMongoId(), async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -191,7 +193,7 @@ router.put('/:id', authenticateToken, requireSensitive2FA, param('id').isMongoId
// POST /api/payments/:id/refund - Process refund
// Risk-based step-up auth: requireSensitive2FA for payout changes
-router.post('/:id/refund', authenticateToken, requireSensitive2FA, param('id').isMongoId(), async (req, res) => {
+router.post('/:id/refund', authenticateToken, zeroTrustAuth, zeroTrustPolicy, requireSensitive2FA, param('id').isMongoId(), async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -228,7 +230,7 @@ router.post('/:id/refund', authenticateToken, requireSensitive2FA, param('id').i
// POST /api/payments/:id/reconcile - Mark payment as reconciled
// Risk-based step-up auth: requireSensitive2FA for payout changes
-router.post('/:id/reconcile', authenticateToken, requireSensitive2FA, param('id').isMongoId(), async (req, res) => {
+router.post('/:id/reconcile', authenticateToken, zeroTrustAuth, zeroTrustPolicy, requireSensitive2FA, param('id').isMongoId(), async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -251,7 +253,7 @@ router.post('/:id/reconcile', authenticateToken, requireSensitive2FA, param('id'
// POST /api/payments/reconcile/bulk - Reconcile multiple payments
// Risk-based step-up auth: requireSensitive2FA for payout changes
-router.post('/reconcile/bulk', authenticateToken, requireSensitive2FA, async (req, res) => {
+router.post('/reconcile/bulk', authenticateToken, zeroTrustAuth, zeroTrustPolicy, requireSensitive2FA, async (req, res) => {
try {
const { payment_ids } = req.body;
@@ -278,7 +280,7 @@ router.post('/reconcile/bulk', authenticateToken, requireSensitive2FA, async (re
});
// GET /api/payments/:id/receipt - Generate and download receipt PDF
-router.get('/:id/receipt', authenticateToken, param('id').isMongoId(), async (req, res) => {
+router.get('/:id/receipt', authenticateToken, zeroTrustAuth, zeroTrustPolicy, param('id').isMongoId(), async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -297,7 +299,7 @@ router.get('/:id/receipt', authenticateToken, param('id').isMongoId(), async (re
});
// GET /api/payments/client/:clientId/history - Get payment history for a client
-router.get('/client/:clientId/history', authenticateToken, param('clientId').isMongoId(), async (req, res) => {
+router.get('/client/:clientId/history', authenticateToken, zeroTrustAuth, zeroTrustPolicy, param('clientId').isMongoId(), async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
diff --git a/utils/auditLogger.js b/utils/auditLogger.js
new file mode 100644
index 00000000..9063b1ab
--- /dev/null
+++ b/utils/auditLogger.js
@@ -0,0 +1,16 @@
+// Audit Logging Utility
+const fs = require('fs');
+const path = require('path');
+const logFile = path.join(__dirname, '../logs/access.log');
+
+function log(message) {
+ fs.appendFileSync(logFile, message + '\n');
+}
+
+function requestLogger(req, res, next) {
+ const entry = `${new Date().toISOString()} ${req.method} ${req.path} user=${req.user ? req.user.id : 'anon'} risk=${req.riskScore || 0}`;
+ log(entry);
+ next();
+}
+
+module.exports = { log, requestLogger };
diff --git a/utils/logger.js b/utils/logger.js
index 9ff034fd..9063b1ab 100644
--- a/utils/logger.js
+++ b/utils/logger.js
@@ -1,30 +1,16 @@
-const winston = require('winston');
+// Audit Logging Utility
+const fs = require('fs');
+const path = require('path');
+const logFile = path.join(__dirname, '../logs/access.log');
-// Create logger instance
-const logger = winston.createLogger({
- level: process.env.LOG_LEVEL || 'info',
- format: winston.format.combine(
- winston.format.timestamp(),
- winston.format.errors({ stack: true }),
- winston.format.json()
- ),
- defaultMeta: { service: 'expense-flow' },
- transports: [
- // Write all logs with importance level of `error` or less to `error.log`
- new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
- // Write all logs with importance level of `info` or less to `combined.log`
- new winston.transports.File({ filename: 'logs/combined.log' }),
- ],
-});
+function log(message) {
+ fs.appendFileSync(logFile, message + '\n');
+}
-// If we're not in production then log to the console with a simple format
-if (process.env.NODE_ENV !== 'production') {
- logger.add(new winston.transports.Console({
- format: winston.format.combine(
- winston.format.colorize(),
- winston.format.simple()
- )
- }));
+function requestLogger(req, res, next) {
+ const entry = `${new Date().toISOString()} ${req.method} ${req.path} user=${req.user ? req.user.id : 'anon'} risk=${req.riskScore || 0}`;
+ log(entry);
+ next();
}
-module.exports = logger;
+module.exports = { log, requestLogger };