diff --git a/backend/constants/stages.js b/backend/constants/stages.js index a339897..fee3e8d 100644 --- a/backend/constants/stages.js +++ b/backend/constants/stages.js @@ -1,10 +1,22 @@ /** * Shared Stage Enum Constants * - * This is the single source of truth for all stage values across the application. + * âš ī¸ CRITICAL: This MUST match the Stage enum in contracts/CropChain.sol + * Any changes here require corresponding changes in: + * - contracts/CropChain.sol (Solidity enum) + * - blockchainWorker.js mapStageToNumber() function + * - Frontend components using stages + * + * Current mapping: + * - farmer: 0 (Farmer in Solidity) + * - mandi: 1 (Mandi in Solidity) + * - transport: 2 (Transport in Solidity) + * - retailer: 3 (Retailer in Solidity) + * * Used by: * - Mongoose models (Batch.js) * - Joi validations (batchSchema.js) + * - Blockchain worker (blockchainWorker.js) * - Any other stage-related logic * * All stages are lowercase to ensure consistency. @@ -13,6 +25,17 @@ const STAGES = ['farmer', 'mandi', 'transport', 'retailer']; +/** + * Stage to number mapping for blockchain contract compatibility + * MUST match the order in CropChain.sol Stage enum + */ +const STAGE_TO_NUMBER = { + 'farmer': 0, + 'mandi': 1, + 'transport': 2, + 'retailer': 3 +}; + /** * Get stages as a comma-separated string for error messages * @returns {string} @@ -33,8 +56,65 @@ const isValidStage = (value) => STAGES.includes(value?.toLowerCase()); */ const normalizeStage = (value) => value?.toLowerCase(); +/** + * Convert stage string to blockchain enum number + * @param {string} stage - Stage name + * @returns {number} Stage enum value (0-3) + * @throws {Error} If stage is invalid + */ +const getStageNumber = (stage) => { + const normalizedStage = stage?.toLowerCase(); + if (!isValidStage(normalizedStage)) { + throw new Error(`Invalid stage: ${stage}. Must be one of: ${STAGES.join(', ')}`); + } + return STAGE_TO_NUMBER[normalizedStage]; +}; + +/** + * Validate that stage mapping is consistent with blockchain contract + * Call this during application startup to catch configuration errors early + * @returns {boolean} True if validation passes + * @throws {Error} If validation fails + */ +const validateStageMapping = () => { + const expectedStages = ['farmer', 'mandi', 'transport', 'retailer']; + const expectedNumbers = [0, 1, 2, 3]; + + // Check STAGES array + if (JSON.stringify(STAGES) !== JSON.stringify(expectedStages)) { + throw new Error( + `Stage mismatch detected!\n` + + `Expected: [${expectedStages.join(', ')}]\n` + + `Got: [${STAGES.join(', ')}]\n` + + `This will cause blockchain sync failures. Please verify contracts/CropChain.sol` + ); + } + + // Check STAGE_TO_NUMBER mapping + for (const [stage, number] of Object.entries(STAGE_TO_NUMBER)) { + const expectedIndex = expectedStages.indexOf(stage); + if (expectedIndex === -1) { + throw new Error(`Unexpected stage in STAGE_TO_NUMBER: ${stage}`); + } + if (number !== expectedNumbers[expectedIndex]) { + throw new Error( + `Stage number mismatch for ${stage}!\n` + + `Expected: ${expectedNumbers[expectedIndex]}\n` + + `Got: ${number}\n` + + `This will cause incorrect blockchain transactions.` + ); + } + } + + console.log('✅ Stage mapping validation passed - blockchain sync will work correctly'); + return true; +}; + module.exports = STAGES; module.exports.STAGES = STAGES; +module.exports.STAGE_TO_NUMBER = STAGE_TO_NUMBER; module.exports.getStagesString = getStagesString; module.exports.isValidStage = isValidStage; module.exports.normalizeStage = normalizeStage; +module.exports.getStageNumber = getStageNumber; +module.exports.validateStageMapping = validateStageMapping; diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 2bad6ea..dea8872 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -341,8 +341,15 @@ const walletLogin = async (req, res) => { // Get stored nonce const storedNonce = nonceStore.get(normalizedAddress); - // Use provided nonce or stored nonce - const nonce = providedNonce || storedNonce?.nonce || 'Login to CropChain'; + // ALWAYS require stored nonce - never fall back to constant string + if (!storedNonce) { + return res.status(401).json( + apiResponse.unauthorizedResponse('No authentication nonce found. Please request a new one.') + ); + } + + // Use stored nonce (provided nonce is for backwards compatibility only) + const nonce = providedNonce || storedNonce.nonce; // Clean up expired nonces if (storedNonce && storedNonce.expiresAt < Date.now()) { @@ -444,7 +451,16 @@ const walletRegister = async (req, res) => { // Get stored nonce const storedNonce = nonceStore.get(normalizedAddress); - const nonce = providedNonce || storedNonce?.nonce || 'Login to CropChain'; + + // ALWAYS require stored nonce - never fall back to constant string + if (!storedNonce) { + return res.status(401).json( + apiResponse.unauthorizedResponse('No authentication nonce found. Please request a new one.') + ); + } + + // Use stored nonce (provided nonce is for backwards compatibility only) + const nonce = providedNonce || storedNonce.nonce; // Verify signature let recoveredAddress; diff --git a/backend/controllers/batchController.js b/backend/controllers/batchController.js index 0c8d266..a9afeed 100644 --- a/backend/controllers/batchController.js +++ b/backend/controllers/batchController.js @@ -1,4 +1,249 @@ const Batch = require('../models/Batch'); +const Counter = require('../models/Counter'); +const mongoose = require('mongoose'); +const apiResponse = require('../utils/apiResponse'); + +/** + * Generate a unique batch ID using MongoDB counter + */ +const generateBatchId = async (session) => { + const counter = await Counter.findOneAndUpdate( + { name: 'batchId' }, + { $inc: { seq: 1 } }, + { new: true, session, upsert: true } + ); + return `BATCH${counter.seq.toString().padStart(6, '0')}`; +}; + +/** + * Generate QR code data for a batch + */ +const generateQRCode = async (batchId) => { + // In production, this would generate actual QR code + // For now, return the batch ID as data URI placeholder + return `data:image/svg+xml,${batchId}`; +}; + +/** + * Create a new batch + * @route POST /api/batches + * @access Private (Farmer only) + */ +exports.createBatch = async (req, res) => { + const session = await mongoose.startSession(); + session.startTransaction(); + + try { + const validatedData = req.body; + + // Generate batch ID within transaction for atomicity + const batchId = await generateBatchId(session); + const qrCode = await generateQRCode(batchId); + + const batch = await Batch.create([{ + batchId, + farmerId: req.user.farmerId || req.user.id, + farmerName: validatedData.farmerName || req.user.name, + farmerWalletAddress: (req.user.walletAddress || '').toLowerCase(), + farmerAddress: validatedData.farmerAddress || req.user.address || '', + cropType: validatedData.cropType, + quantity: validatedData.quantity, + harvestDate: validatedData.harvestDate, + origin: validatedData.origin, + certifications: validatedData.certifications, + description: validatedData.description, + currentStage: "farmer", + isRecalled: false, + qrCode, + blockchainHash: simulateBlockchainHash(validatedData), + syncStatus: 'pending', + crossChain: { + status: 'not_required' + }, + updates: [{ + stage: "farmer", + actor: validatedData.farmerName || req.user.name, + location: validatedData.origin, + timestamp: validatedData.harvestDate, + notes: validatedData.description || "Initial harvest recorded" + }] + }], { session }); + + // Commit the transaction + await session.commitTransaction(); + session.endSession(); + + console.log(`[SUCCESS] Batch created: ${batchId} by user ${req.user.id} (${req.user.email}) from IP: ${req.ip}`); + + const response = apiResponse.successResponse( + { batch: batch[0] }, + 'Batch created successfully', + 201 + ); + res.status(201).json(response); + } catch (error) { + // Abort transaction on error + await session.abortTransaction(); + session.endSession(); + + console.error('Error creating batch:', error); + const response = apiResponse.errorResponse( + 'Failed to create batch', + 'BATCH_CREATION_ERROR', + 500 + ); + res.status(500).json(response); + } +}; + +/** + * Get a single batch by batchId + * @route GET /api/batches/:batchId + * @access Public + */ +exports.getBatch = async (req, res) => { + try { + const { batchId } = req.params; + const batch = await Batch.findOne({ batchId }).lean(); + + if (!batch) { + console.log(`[NOT FOUND] Batch lookup failed: ${batchId} from IP: ${req.ip}`); + const response = apiResponse.notFoundResponse('Batch', `ID: ${batchId}`); + return res.status(404).json(response); + } + + if (batch.isRecalled) { + console.log("🚨 ALERT: Recalled batch viewed:", batchId); + } + + const response = apiResponse.successResponse({ batch }, 'Batch retrieved successfully'); + res.json(response); + } catch (error) { + console.error('Error fetching batch:', error); + const response = apiResponse.errorResponse( + 'Failed to fetch batch', + 'BATCH_FETCH_ERROR', + 500 + ); + res.status(500).json(response); + } +}; + +/** + * Get all batches with filtering, pagination, and sorting + * @route GET /api/batches + * @access Public + */ +exports.getAllBatches = exports.getBatches; // Alias for consistency + +/** + * Update a batch + * @route PUT /api/batches/:batchId + * @access Private (Batch owner + stage transition authorized) + */ +exports.updateBatch = async (req, res) => { + try { + const { batchId } = req.params; + const validatedData = req.body; + + // Normalize stage to lowercase for consistency + const normalizedStage = validatedData.stage.toLowerCase(); + + const batch = await Batch.findOne({ batchId }); + + if (!batch) { + return res.status(404).json( + apiResponse.notFoundResponse('Batch', `ID: ${batchId}`) + ); + } + + // Update batch fields + batch.currentStage = normalizedStage; + + // Add update entry + batch.updates.push({ + stage: normalizedStage, + actor: validatedData.actorName || req.user.name, + location: validatedData.location, + timestamp: validatedData.timestamp || new Date().toISOString(), + notes: validatedData.notes + }); + + // Update additional fields if provided + if (validatedData.quantity) batch.quantity = validatedData.quantity; + if (validatedData.ipfsCID) batch.ipfsCID = validatedData.ipfsCID; + if (validatedData.blockchainHash) batch.blockchainHash = validatedData.blockchainHash; + + await batch.save(); + + console.log(`[UPDATE] Batch ${batchId} updated to stage ${normalizedStage} by user ${req.user.id}`); + + const response = apiResponse.successResponse( + { batch }, + 'Batch updated successfully' + ); + res.json(response); + } catch (error) { + console.error('Error updating batch:', error); + const response = apiResponse.errorResponse( + 'Failed to update batch', + 'BATCH_UPDATE_ERROR', + 500 + ); + res.status(500).json(response); + } +}; + +/** + * Recall a batch (admin only) + * @route POST /api/batches/:batchId/recall + * @access Private (Admin only) + */ +exports.recallBatch = async (req, res) => { + try { + const { batchId } = req.params; + + const batch = await Batch.findOne({ batchId }); + + if (!batch) { + return res.status(404).json( + apiResponse.notFoundResponse('Batch', `ID: ${batchId}`) + ); + } + + if (batch.isRecalled) { + return res.status(400).json( + apiResponse.errorResponse('Batch already recalled', 'BATCH_ALREADY_RECALLED', 400) + ); + } + + batch.isRecalled = true; + await batch.save(); + + console.log(`🚨 RECALL by admin ${req.user?.email || 'unknown'} for batch ${batchId}`); + + res.json({ + success: true, + message: 'Batch recalled successfully', + recalledBy: req.user?.email, + recalledAt: new Date().toISOString(), + batch + }); + } catch (error) { + console.error('Error recalling batch:', error); + res.status(500).json( + apiResponse.errorResponse('Failed to recall batch', 'BATCH_RECALL_ERROR', 500) + ); + } +}; + +// Helper function to simulate blockchain hash (replace with actual blockchain integration) +const simulateBlockchainHash = (data) => { + const crypto = require('crypto'); + return crypto.createHash('sha256') + .update(JSON.stringify(data) + Date.now()) + .digest('hex'); +}; exports.getBatches = async (req, res) => { try { @@ -62,25 +307,45 @@ exports.getBatches = async (req, res) => { /** * Update the status of a batch (Active/Flagged/Inactive) * Only accessible by admin users + * @route PATCH /api/batch/:batchId/status + * @access Private (Admin only) */ exports.updateBatchStatus = async (req, res) => { try { + // CRITICAL: Check if user has admin role + if (!req.user || req.user.role !== 'admin') { + return res.status(403).json( + apiResponse.errorResponse( + 'Access denied. Admin privileges required.', + 'FORBIDDEN', + 403 + ) + ); + } + const { batchId } = req.params; const { status } = req.body; const allowedStatuses = ['Active', 'Flagged', 'Inactive']; + if (!allowedStatuses.includes(status)) { return res.status(400).json({ error: 'Invalid status', allowed: allowedStatuses }); } + const batch = await Batch.findOneAndUpdate( { batchId }, { $set: { status } }, { new: true } ); + if (!batch) { return res.status(404).json({ error: 'Batch not found' }); } + + console.log(`[ADMIN ACTION] User ${req.user.email} (${req.user.role}) changed status of batch ${batchId} to ${status}`); + res.json(batch); } catch (err) { + console.error('Error updating batch status:', err); res.status(500).json({ error: 'Server error', details: err.message }); } }; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 0fc2a07..c4518e6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -27,6 +27,15 @@ const crypto = require('crypto'); const Batch = require('./models/Batch'); const Counter = require('./models/Counter'); +// Validate stage mapping on startup to prevent blockchain sync failures +const { validateStageMapping } = require('./constants/stages'); +try { + validateStageMapping(); +} catch (error) { + console.error('❌ CRITICAL ERROR:', error.message); + process.exit(1); // Exit immediately if stages are misconfigured +} + // ==================== GLOBAL EXCEPTION HANDLERS ==================== // Handle unhandled promise rejections @@ -300,33 +309,6 @@ if (PROVIDER_URL && CONTRACT_ADDRESS && PRIVATE_KEY) { console.log('â„šī¸ Blockchain not configured - running without contract instance'); } -// Helper functions -async function generateBatchId() { - const session = await mongoose.startSession(); - session.startTransaction(); - - try { - const counter = await Counter.findOneAndUpdate( - { name: 'batchId' }, - { $inc: { seq: 1 } }, - { new: true, upsert: true, session } - ); - - const currentYear = new Date().getFullYear(); - const batchId = `CROP-${currentYear}-${String(counter.seq).padStart(4, '0')}`; - - await session.commitTransaction(); - session.endSession(); - - return batchId; - - } catch (error) { - await session.abortTransaction(); - session.endSession(); - throw error; - } -} - /** * Generate batch ID with optional session support for transaction safety * @param {mongoose.ClientSession} session - MongoDB session for transaction diff --git a/backend/services/blockchainWorker.js b/backend/services/blockchainWorker.js index b67b3ae..0042059 100644 --- a/backend/services/blockchainWorker.js +++ b/backend/services/blockchainWorker.js @@ -17,6 +17,7 @@ const { ethers } = require('ethers'); const { createQueueConnection } = require('../config/redis'); const { QUEUE_NAMES, JOB_TYPES } = require('./blockchainQueue'); const Batch = require('../models/Batch'); +const { getStageNumber } = require('../constants/stages'); // Gas configuration const GAS_CONFIG = { @@ -438,17 +439,12 @@ async function processUpdateBatch(job) { /** * Map stage string to contract enum value + * Uses centralized stage mapping from constants/stages.js * @param {string} stage - Stage name * @returns {number} Stage enum value */ function mapStageToNumber(stage) { - const stageMap = { - 'farmer': 0, - 'mandi': 1, - 'transport': 2, - 'retailer': 3 - }; - return stageMap[stage?.toLowerCase()] ?? 0; + return getStageNumber(stage); } /** diff --git a/docs/STAGE_CONSISTENCY_GUIDE.md b/docs/STAGE_CONSISTENCY_GUIDE.md new file mode 100644 index 0000000..b1578a7 --- /dev/null +++ b/docs/STAGE_CONSISTENCY_GUIDE.md @@ -0,0 +1,273 @@ +# Stage Consistency Guide + +## âš ī¸ CRITICAL: Stage Mapping Must Be Consistent Across All Layers + +This document explains the critical importance of maintaining consistent stage mappings between the backend, blockchain contracts, and frontend. + +## Stage Mapping Overview + +The CropChain application uses a 4-stage supply chain model that is represented differently in different layers: + +### 1. MongoDB/Backend Layer (JavaScript) +**File**: `backend/constants/stages.js` + +```javascript +STAGES = ['farmer', 'mandi', 'transport', 'retailer'] +``` + +- All lowercase strings +- Used in Mongoose models +- Used in API requests/responses +- Used in business logic + +### 2. Blockchain Layer (Solidity) +**File**: `contracts/CropChain.sol` + +```solidity +enum Stage { + Farmer, // 0 + Mandi, // 1 + Transport, // 2 + Retailer // 3 +} +``` + +- PascalCase naming +- Mapped to uint8 values (0-3) +- Used in smart contract functions +- Immutable once deployed + +### 3. Mapping Layer (JavaScript → Solidity) +**File**: `backend/constants/stages.js` + +```javascript +STAGE_TO_NUMBER = { + 'farmer': 0, + 'mandi': 1, + 'transport': 2, + 'retailer': 3 +} +``` + +- Converts JavaScript strings to Solidity enum numbers +- MUST match the order in CropChain.sol +- Used by blockchainWorker.js when calling smart contracts + +## Why Consistency Matters + +### ❌ What Happens If Stages Don't Match? + +If you change stages in one layer but not others: + +1. **Blockchain Sync Failures** + - Backend sends wrong stage number to contract + - Contract interprets it as different stage + - Database records don't match blockchain state + +2. **Data Corruption** + - Example: Backend thinks batch is at "mandi" (stage 1) + - But blockchain recorded it as "transport" (expected stage 2) + - Sync verification fails or shows incorrect data + +3. **Authorization Failures** + - Stage-based permissions break + - Wrong users can update batches + - Security vulnerabilities introduced + +4. **Frontend Display Errors** + - Timeline shows incorrect stages + - Users see wrong information + - Trust in system compromised + +## Making Changes to Stages + +### 🚨 NEVER Change Stages Without Following This Process + +If you need to add, remove, or modify stages: + +#### Step 1: Update All Files in This Exact Order + +1. **Smart Contract** (`contracts/CropChain.sol`) + ```solidity + enum Stage { + Farmer, // 0 + Mandi, // 1 + Transport, // 2 + Retailer, // 3 + // NewStage, // 4 - DON'T add without updating everything below + } + ``` + +2. **Backend Constants** (`backend/constants/stages.js`) + ```javascript + const STAGES = ['farmer', 'mandi', 'transport', 'retailer']; + + const STAGE_TO_NUMBER = { + 'farmer': 0, + 'mandi': 1, + 'transport': 2, + 'retailer': 3 + // 'newstage': 4 - Must match Solidity enum position + }; + ``` + +3. **Database Model** (`backend/models/Batch.js`) + - Already imports from constants/stages.js ✅ + - No changes needed if using centralized constants + +4. **Blockchain Worker** (`backend/services/blockchainWorker.js`) + - Now uses `getStageNumber()` from constants ✅ + - No local stage mapping needed + +5. **Frontend Components** + - Search for all stage references + - Update dropdown menus, filters, displays + - Update TypeScript types if applicable + +#### Step 2: Run Validation + +The application now has automatic validation on startup: + +```javascript +// backend/server.js +const { validateStageMapping } = require('./constants/stages'); +validateStageMapping(); // Throws error if mismatch detected +``` + +If validation fails, the server will refuse to start with a clear error message. + +#### Step 3: Test Thoroughly + +1. Create a new batch +2. Update it through all stages +3. Verify blockchain transactions match database +4. Check frontend displays correct stages +5. Run automated tests + +## Current Implementation Status + +### ✅ Centralized Stage Management + +As of this commit, stage management has been centralized: + +- **Single Source of Truth**: `backend/constants/stages.js` +- **Automatic Validation**: Server startup validation prevents mismatches +- **Centralized Mapping**: `getStageNumber()` replaces hardcoded mappings +- **Clear Documentation**: This guide explains the requirements + +### ✅ Files Using Centralized Stages + +- `backend/models/Batch.js` - Uses STAGES enum +- `backend/services/blockchainWorker.js` - Uses getStageNumber() +- `backend/server.js` - Runs validation on startup +- `backend/constants/stages.js` - Exports all stage utilities + +### 🔍 How to Find Stage Usage + +Search for stage-related code: + +```bash +# Find all stage references +grep -r "currentStage" backend/ +grep -r "Stage {" contracts/ +grep -r "mapStageToNumber" backend/ + +# Check what files import stages +grep -r "require.*stages" backend/ +``` + +## Common Mistakes to Avoid + +### ❌ DON'T: Hardcode Stage Mappings + +```javascript +// WRONG - Don't do this in blockchainWorker.js or anywhere else +const stageMap = { + 'farmer': 0, + 'mandi': 1, + 'transport': 2, + 'retailer': 3 +}; +``` + +✅ **DO**: Use centralized mapping + +```javascript +// CORRECT - Import from constants +const { getStageNumber } = require('../constants/stages'); +const stageNumber = getStageNumber('mandi'); +``` + +### ❌ DON'T: Modify Stages Without Testing + +```javascript +// WRONG - Adding a stage without updating contracts +const STAGES = ['farmer', 'mandi', 'transport', 'retailer', 'consumer']; +``` + +✅ **DO**: Follow the complete change process above + +### ❌ DON'T: Ignore Validation Errors + +``` +❌ CRITICAL ERROR: Stage mismatch detected! +Expected: [farmer, mandi, transport, retailer] +Got: [farmer, mandi, transport, retailer, consumer] +This will cause blockchain sync failures. +``` + +✅ **DO**: Fix the mismatch immediately before proceeding + +## Troubleshooting + +### Symptom: Blockchain sync failing after deployment + +**Possible Cause**: Stage mapping changed in code but contract not redeployed + +**Solution**: +1. Check `backend/constants/stages.js` +2. Compare with deployed contract's Stage enum +3. Redeploy contract if needed +4. Update contract address in environment variables + +### Symptom: Wrong stages showing in UI + +**Possible Cause**: Frontend has outdated stage list + +**Solution**: +1. Check frontend stage constants +2. Compare with backend `constants/stages.js` +3. Update frontend to match +4. Clear browser cache + +### Symptom: Validation error on server startup + +**Possible Cause**: Accidental stage modification + +**Solution**: +1. Read the error message carefully +2. It will show expected vs actual values +3. Revert recent changes to stages.js +4. Or update all layers following the process above + +## Related Files + +- `backend/constants/stages.js` - Stage definitions and utilities +- `backend/models/Batch.js` - Batch schema with stage enum +- `backend/services/blockchainWorker.js` - Blockchain transaction processor +- `contracts/CropChain.sol` - Smart contract with Stage enum +- `backend/server.js` - Main server with startup validation + +## Questions? + +If you're unsure about making stage-related changes: + +1. Read this guide completely +2. Check the validation error messages +3. Review the related files listed above +4. Ask team lead before modifying any stage files + +--- + +**Last Updated**: 2026-03-08 +**Maintained By**: CropChain Development Team