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
13 changes: 3 additions & 10 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
name: FarmSmart CI/CD Pipeline

# Trigger the workflow on pushes and pull requests to the main branch
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

# Define environment variables if needed
env:
NODE_VERSION: '18.x'

jobs:
# ------------------------------------------------------------------

# JOB 1: BACKEND CI (Test & Build)
# ------------------------------------------------------------------
backend-ci:
name: Backend CI (Test & Build)
runs-on: ubuntu-latest
Expand All @@ -40,15 +37,13 @@ jobs:
# - name: Run Tests (Jest)
# run: npm test -- --passWithNoTests

# - name: Build / Type Check (TypeScript)
# run: npm run build
- name: Build / Type Check (TypeScript)
run: npm run build

- name: Build Project (Simulated)
run: echo "Build successful"

# ------------------------------------------------------------------
# JOB 2: FRONTEND CI (Lint & Build)
# ------------------------------------------------------------------
frontend-ci:
name: Frontend CI (Lint & Build)
runs-on: ubuntu-latest
Expand Down Expand Up @@ -78,9 +73,7 @@ jobs:
- name: Build Project (Vite)
run: npm run build

# ------------------------------------------------------------------
# JOB 3: DEPLOY TO RENDER (CD)
# ------------------------------------------------------------------
deploy:
name: Deploy to Render
needs: [backend-ci, frontend-ci] # Only deploy if CI passes
Expand Down
20 changes: 20 additions & 0 deletions backend/__tests__/crops/crop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const validCrop = {
quantity: 100,
unit: 'kg',
basePrice: 25.50,
finalPrice: 25.50,
imageUrl: 'https://example.com/tomato.jpg',
qualityGrade: 'A',
location: {
state: 'Maharashtra',
Expand All @@ -48,6 +50,8 @@ const invalidCrop = {
quantity: -50, // INVALID: Negative quantity
unit: 'kg',
basePrice: -100, // INVALID: Negative price
finalPrice: 100,
imageUrl: 'img',
qualityGrade: 'A',
location: {
state: 'Punjab',
Expand All @@ -69,6 +73,8 @@ const cropEmptyName = {
quantity: 50,
unit: 'kg',
basePrice: 20,
finalPrice: 20,
imageUrl: 'img',
qualityGrade: 'B',
location: {
state: 'Madhya Pradesh',
Expand All @@ -81,6 +87,8 @@ const cropInvalidUnit = {
quantity: 75,
unit: 'bags', // INVALID: Only kg, quintal, ton allowed
basePrice: 30,
finalPrice: 30,
imageUrl: 'img',
qualityGrade: 'B',
location: {
state: 'Rajasthan',
Expand All @@ -93,6 +101,8 @@ const cropInvalidGrade = {
quantity: 60,
unit: 'quintal',
basePrice: 28,
finalPrice: 28,
imageUrl: 'img',
qualityGrade: 'D', // INVALID: Only A, B, C allowed
location: {
state: 'Telangana',
Expand All @@ -105,6 +115,8 @@ const zeroQuantityCrop = {
quantity: 0,
unit: 'ton',
basePrice: 15,
finalPrice: 15,
imageUrl: 'http://img.com/barley.jpg',
qualityGrade: 'C',
location: {
state: 'Uttarakhand',
Expand All @@ -117,6 +129,8 @@ const zeroPrice = {
quantity: 200,
unit: 'kg',
basePrice: 0, // EDGE CASE: Zero price
finalPrice: 0,
imageUrl: 'http://img.com/soy.jpg',
qualityGrade: 'B',
location: {
state: 'Madhya Pradesh',
Expand All @@ -129,6 +143,8 @@ const largeQuantity = {
quantity: 999999999, // EDGE CASE: Very large number
unit: 'ton',
basePrice: 3.50,
finalPrice: 3.50,
imageUrl: 'http://img.com/cane.jpg',
qualityGrade: 'A',
location: {
state: 'Uttar Pradesh',
Expand All @@ -141,6 +157,8 @@ const duplicateCrop = {
quantity: 150,
unit: 'quintal',
basePrice: 35,
finalPrice: 35,
imageUrl: 'http://img.com/potatoes.jpg',
qualityGrade: 'B',
location: {
state: 'Karnataka',
Expand Down Expand Up @@ -212,6 +230,8 @@ describe('CROP CONTROLLER - COMPREHENSIVE TEST SUITE', () => {
quantity: 500,
unit: 'kg',
basePrice: 40,
finalPrice: 40,
imageUrl: 'http://img.com/rice.jpg',
qualityGrade: 'A',
location: {
state: 'Punjab',
Expand Down
110 changes: 0 additions & 110 deletions backend/src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import User from '../models/User';
import VerificationCode, { VerificationType } from '../models/VerificationCode';
import { sendResponse } from '../utils/response';
import { AuthRequest } from '../middleware/authMiddleware';
import { sendWelcomeMessage, sendLoginAlert } from '../services/notificationService';
Expand Down Expand Up @@ -104,30 +103,7 @@ export const updateProfile = async (req: AuthRequest, res: Response): Promise<vo
}
};

const generateOTP = (): string => {
return Math.floor(100000 + Math.random() * 900000).toString();
};

const saveOTP = async (contact: string, type: VerificationType) => {
const code = generateOTP();
const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes

// Delete existing codes for this user/type
await VerificationCode.deleteMany({ contact, type });

await VerificationCode.create({
contact,
code,
type,
expiresAt
});

// In a real app, send SMS here
console.log(`\n----------------------------------------`);
console.log(`[OTP] GENERATED FOR ${contact}: ${code}`);
console.log(`----------------------------------------\n`);
return code;
};

export const register = async (req: Request, res: Response): Promise<void> => {
try {
Expand Down Expand Up @@ -253,89 +229,3 @@ export const login = async (req: Request, res: Response): Promise<void> => {
}
};

export const verify = async (req: Request, res: Response): Promise<void> => {
try {
const { contact, code } = req.body;

if (!contact || !code) {
sendResponse(res, 400, "Contact and code are required");
return;
}

// 1. Find the OTP record
const record = await VerificationCode.findOne({
contact: contact,
code: code,
type: VerificationType.PHONE,
expiresAt: { $gt: new Date() } // check if not expired
});

if (!record) {
sendResponse(res, 400, "Invalid or expired OTP");
return;
}

// 2. Find the user
const user = await User.findOne({ phoneNumber: contact });
if (!user) {
sendResponse(res, 404, "User not found");
return;
}

// 3. Mark user as verified
user.isVerified = true;
await user.save();

// 4. Delete the OTP record (prevent reuse)
await VerificationCode.deleteOne({ _id: record._id });

// 5. Generate Token
const token = jwt.sign(
{ userId: user._id, role: user.role },
JWT_SECRET,
{ expiresIn: '7d' }
);

// Exclude password from response
const userObject = user.toObject();
const { passwordHash: _, ...userWithoutPassword } = userObject;

sendResponse(res, 200, "Verification successful", {
user: userWithoutPassword,
token,
});

} catch (error) {
console.error("Verify Error:", error);
sendResponse(res, 500, "Internal Server Error");
}
};

export const resendOtp = async (req: Request, res: Response): Promise<void> => {
try {
const { contact } = req.body;

if (!contact) {
sendResponse(res, 400, "Contact is required");
return;
}

// 1. Check if user exists
const user = await User.findOne({ phoneNumber: contact });
if (!user) {
sendResponse(res, 404, "User not found");
return;
}

// 2. Generate and Save OTP
const otp = await saveOTP(contact, VerificationType.PHONE);

sendResponse(res, 200, "OTP resent successfully", {
debugOtp: otp
});

} catch (error) {
console.error("Resend OTP Error:", error);
sendResponse(res, 500, "Internal Server Error");
}
};
Loading
Loading