Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion backend/constants/stages.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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}
Expand All @@ -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;
39 changes: 30 additions & 9 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const crypto = require('crypto');
const { z } = require('zod');
const apiResponse = require('../utils/apiResponse');
const { verifyMessage } = require('ethers');
const { VALID_ROLES, ROLES } = require('../constants/permissions');
require('dotenv').config();

// Validation Schemas
Expand All @@ -24,9 +25,11 @@ const registerSchema = z.object({
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
role: z.enum(['farmer', 'mandi', 'transporter', 'retailer'], {
errorMap: () => ({ message: 'Invalid role. Only farmer, mandi, transporter, and retailer are allowed.' })
})
role: z.enum(VALID_ROLES, {
errorMap: () => ({
message: `Invalid role. Must be one of: ${VALID_ROLES.join(', ')}`
})
}).default(ROLES.FARMER)
});

const updateProfileSchema = z.object({
Expand Down Expand Up @@ -341,8 +344,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()) {
Expand Down Expand Up @@ -421,9 +431,11 @@ const walletRegisterSchema = z.object({
signature: z.string()
.min(1, 'Signature is required'),
nonce: z.string().optional(),
role: z.enum(['farmer', 'mandi', 'transporter', 'retailer'], {
errorMap: () => ({ message: 'Invalid role. Only farmer, mandi, transporter, and retailer are allowed.' })
})
role: z.enum(VALID_ROLES, {
errorMap: () => ({
message: `Invalid role. Must be one of: ${VALID_ROLES.join(', ')}`
})
}).default(ROLES.FARMER)
});

const walletRegister = async (req, res) => {
Expand All @@ -444,7 +456,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;
Expand Down
Loading
Loading