From f58b7b8847ec5d311ec83d0a598bb6ebf22b36ec Mon Sep 17 00:00:00 2001 From: AdeepaK2 Date: Tue, 2 Sep 2025 14:32:14 +0530 Subject: [PATCH] Implement JWT authentication and authorization across API endpoints - Added JWT authentication to meeting notes retrieval, ensuring users can only access their own notes. - Enhanced meeting retrieval and modification endpoints to validate user identity and permissions. - Implemented authentication checks for meeting cancellation and acknowledgment, restricting actions to authorized users. - Updated message handling endpoints to enforce user authentication and ensure only participants can send or modify messages. - Created load tests for authenticated and basic scenarios to evaluate API performance under various conditions. - Refactored API service functions to include authentication headers for secure requests. --- load-tests/auth-load-test.yml | 62 +++++++++++ load-tests/basic-load-test.yml | 53 ++++++++++ load-tests/run-tests.bat | 94 ++++++++++++++++ load-tests/run-tests.sh | 81 ++++++++++++++ load-tests/stress-test.yml | 35 ++++++ src/app/api/meeting-notes/route.ts | 55 ++++++++++ src/app/api/meeting-notes/user/route.ts | 19 ++++ src/app/api/meeting/[id]/route.ts | 29 ++++- src/app/api/meeting/cancel/route.ts | 27 ++++- .../meeting/cancellation/acknowledge/route.ts | 24 ++++- src/app/api/meeting/cancellation/route.ts | 42 +++++++- .../cancellation/unacknowledged/route.ts | 19 +++- src/app/api/meeting/reject/route.ts | 16 ++- src/app/api/meeting/route.ts | 46 +++++++- src/app/api/messages/route.ts | 100 +++++++++++++++++- src/components/messageSystem/MessageInput.tsx | 11 ++ src/services/chatApiServices.ts | 32 ++++-- src/services/meetingApiServices.ts | 65 +++++++++--- 18 files changed, 771 insertions(+), 39 deletions(-) create mode 100644 load-tests/auth-load-test.yml create mode 100644 load-tests/basic-load-test.yml create mode 100644 load-tests/run-tests.bat create mode 100644 load-tests/run-tests.sh create mode 100644 load-tests/stress-test.yml diff --git a/load-tests/auth-load-test.yml b/load-tests/auth-load-test.yml new file mode 100644 index 00000000..8a4ef4c4 --- /dev/null +++ b/load-tests/auth-load-test.yml @@ -0,0 +1,62 @@ +config: + target: 'http://localhost:3000' + phases: + # Test with authenticated users + - duration: 60 + arrivalRate: 10 + name: "Authenticated user load" + - duration: 120 + arrivalRate: 20 + name: "Heavy authenticated load" + + # Mock JWT tokens for testing (you'll need to replace with real tokens) + variables: + authToken: "your-jwt-token-here" + +scenarios: + - name: "Authenticated API Testing" + weight: 100 + flow: + # Test authenticated endpoints + - post: + url: "/api/auth/login" + headers: + Content-Type: "application/json" + json: + email: "test@example.com" + password: "testpassword" + capture: + - json: "$.token" + as: "authToken" + expect: + - statusCode: [200, 401, 404] + + - think: 1 + + # Test messages API with auth + - get: + url: "/api/messages" + headers: + Authorization: "Bearer {{ authToken }}" + expect: + - statusCode: [200, 401] + + - think: 2 + + # Test meetings API with auth + - get: + url: "/api/meeting" + headers: + Authorization: "Bearer {{ authToken }}" + expect: + - statusCode: [200, 401] + + - think: 1 + + # Test user profile + - get: + url: "/api/users/profile" + headers: + Authorization: "Bearer {{ authToken }}" + expect: + - statusCode: [200, 401, 400] diff --git a/load-tests/basic-load-test.yml b/load-tests/basic-load-test.yml new file mode 100644 index 00000000..75637d10 --- /dev/null +++ b/load-tests/basic-load-test.yml @@ -0,0 +1,53 @@ +config: + target: 'http://localhost:3000' + phases: + # Warm up phase - 5 users over 30 seconds + - duration: 30 + arrivalRate: 5 + name: "Warm up" + # Ramp up phase - gradually increase to 20 users over 2 minutes + - duration: 120 + arrivalRate: 5 + rampTo: 20 + name: "Ramp up load" + # Sustained load - 20 users for 3 minutes + - duration: 180 + arrivalRate: 20 + name: "Sustained load" + # Peak load test - 50 users for 1 minute + - duration: 60 + arrivalRate: 50 + name: "Peak load" + +scenarios: + - name: "Browse and interact with app" + weight: 100 + flow: + # Test homepage + - get: + url: "/" + expect: + - statusCode: 200 + + # Test API endpoints (without auth for now) + - get: + url: "/api/health" + expect: + - statusCode: [200, 404] # 404 is OK if endpoint doesn't exist + + # Test static assets + - get: + url: "/next.svg" + expect: + - statusCode: 200 + + # Simulate user thinking time + - think: 2 + + # Test another page (if exists) + - get: + url: "/login" + expect: + - statusCode: [200, 404] + + - think: 1 diff --git a/load-tests/run-tests.bat b/load-tests/run-tests.bat new file mode 100644 index 00000000..36ca5e1b --- /dev/null +++ b/load-tests/run-tests.bat @@ -0,0 +1,94 @@ +@echo off +echo 🚀 Artillery Load Testing for Skill-Swap-Hub +echo ============================================ + +REM Check if Artillery is installed +where artillery >nul 2>nul +if %ERRORLEVEL% NEQ 0 ( + echo ❌ Artillery is not installed. Please install it first: + echo npm install -g artillery + pause + exit /b 1 +) + +REM Check if Next.js server is running +powershell -Command "try { Invoke-WebRequest -Uri http://localhost:3000 -UseBasicParsing -ErrorAction Stop } catch { exit 1 }" >nul 2>nul +if %ERRORLEVEL% NEQ 0 ( + echo ❌ Next.js server is not running on localhost:3000 + echo Please start your server first: npm run dev + pause + exit /b 1 +) + +echo ✅ Server is running, starting load tests... +echo. + +echo Select which test to run: +echo 1^) Basic Load Test ^(recommended for first run^) +echo 2^) Authentication Load Test +echo 3^) Stress Test ^(find breaking point^) +echo 4^) Run All Tests ^(this will take ~20 minutes^) +echo. + +set /p choice=Enter your choice (1-4): + +if "%choice%"=="1" goto basic +if "%choice%"=="2" goto auth +if "%choice%"=="3" goto stress +if "%choice%"=="4" goto all +goto invalid + +:basic +echo 📊 Running Basic Load Test... +echo Started at: %date% %time% +echo ---------------------------------------- +artillery run load-tests\basic-load-test.yml +echo ---------------------------------------- +echo ✅ Basic Load Test completed at: %date% %time% +goto end + +:auth +echo 📊 Running Authentication Load Test... +echo Started at: %date% %time% +echo ---------------------------------------- +artillery run load-tests\auth-load-test.yml +echo ---------------------------------------- +echo ✅ Authentication Load Test completed at: %date% %time% +goto end + +:stress +echo 📊 Running Stress Test... +echo Started at: %date% %time% +echo ---------------------------------------- +artillery run load-tests\stress-test.yml +echo ---------------------------------------- +echo ✅ Stress Test completed at: %date% %time% +goto end + +:all +echo 🔥 Running all tests - this will take approximately 20 minutes... +echo 📊 Running Basic Load Test... +artillery run load-tests\basic-load-test.yml +timeout /t 10 /nobreak >nul +echo 📊 Running Authentication Load Test... +artillery run load-tests\auth-load-test.yml +timeout /t 10 /nobreak >nul +echo 📊 Running Stress Test... +artillery run load-tests\stress-test.yml +echo 🎉 All tests completed! +goto end + +:invalid +echo ❌ Invalid choice. Exiting... +pause +exit /b 1 + +:end +echo. +echo 📈 Load testing completed! +echo Check the results above for: +echo - Response times ^(min, max, median, p95, p99^) +echo - Request rates ^(req/sec^) +echo - Error rates +echo - Concurrent user handling capacity +pause diff --git a/load-tests/run-tests.sh b/load-tests/run-tests.sh new file mode 100644 index 00000000..a49fd128 --- /dev/null +++ b/load-tests/run-tests.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +echo "🚀 Artillery Load Testing for Skill-Swap-Hub" +echo "============================================" + +# Check if Artillery is installed +if ! command -v artillery &> /dev/null +then + echo "❌ Artillery is not installed. Please install it first:" + echo "npm install -g artillery" + exit 1 +fi + +# Check if Next.js server is running +if ! curl -s http://localhost:3000 > /dev/null; then + echo "❌ Next.js server is not running on localhost:3000" + echo "Please start your server first: npm run dev" + exit 1 +fi + +echo "✅ Server is running, starting load tests..." +echo "" + +# Function to run a test +run_test() { + local test_name=$1 + local test_file=$2 + + echo "📊 Running $test_name..." + echo "Test file: $test_file" + echo "Started at: $(date)" + echo "----------------------------------------" + + artillery run "$test_file" + + echo "----------------------------------------" + echo "✅ $test_name completed at: $(date)" + echo "" +} + +# Menu for test selection +echo "Select which test to run:" +echo "1) Basic Load Test (recommended for first run)" +echo "2) Authentication Load Test" +echo "3) Stress Test (find breaking point)" +echo "4) Run All Tests (this will take ~20 minutes)" +echo "" + +read -p "Enter your choice (1-4): " choice + +case $choice in + 1) + run_test "Basic Load Test" "load-tests/basic-load-test.yml" + ;; + 2) + run_test "Authentication Load Test" "load-tests/auth-load-test.yml" + ;; + 3) + run_test "Stress Test" "load-tests/stress-test.yml" + ;; + 4) + echo "🔥 Running all tests - this will take approximately 20 minutes..." + run_test "Basic Load Test" "load-tests/basic-load-test.yml" + sleep 10 + run_test "Authentication Load Test" "load-tests/auth-load-test.yml" + sleep 10 + run_test "Stress Test" "load-tests/stress-test.yml" + echo "🎉 All tests completed!" + ;; + *) + echo "❌ Invalid choice. Exiting..." + exit 1 + ;; +esac + +echo "📈 Load testing completed!" +echo "Check the results above for:" +echo "- Response times (min, max, median, p95, p99)" +echo "- Request rates (req/sec)" +echo "- Error rates" +echo "- Concurrent user handling capacity" diff --git a/load-tests/stress-test.yml b/load-tests/stress-test.yml new file mode 100644 index 00000000..ff172c9c --- /dev/null +++ b/load-tests/stress-test.yml @@ -0,0 +1,35 @@ +config: + target: 'http://localhost:3000' + phases: + # Stress test - find breaking point + - duration: 60 + arrivalRate: 10 + name: "Light load" + - duration: 120 + arrivalRate: 50 + name: "Medium load" + - duration: 180 + arrivalRate: 100 + name: "Heavy load" + - duration: 120 + arrivalRate: 200 + name: "Stress load" + - duration: 60 + arrivalRate: 300 + name: "Breaking point" + +scenarios: + - name: "Stress test scenario" + weight: 100 + flow: + - get: + url: "/" + expect: + - statusCode: [200, 500, 503] + + - think: 0.5 + + - get: + url: "/api/health" + expect: + - statusCode: [200, 404, 500, 503] diff --git a/src/app/api/meeting-notes/route.ts b/src/app/api/meeting-notes/route.ts index 6b66cf8f..402b1092 100644 --- a/src/app/api/meeting-notes/route.ts +++ b/src/app/api/meeting-notes/route.ts @@ -1,10 +1,21 @@ import { NextRequest, NextResponse } from 'next/server'; import connect from '@/lib/db'; import MeetingNotes from '@/lib/models/meetingNotesSchema'; +import { validateAndExtractUserId } from '@/utils/jwtAuth'; export async function GET(req: NextRequest) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req); + if (!authResult.isValid) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Invalid or missing token' + }, { status: 401 }); + } + + const authenticatedUserId = authResult.userId; const url = new URL(req.url); const meetingId = url.searchParams.get('meetingId'); const userId = url.searchParams.get('userId'); @@ -12,6 +23,14 @@ export async function GET(req: NextRequest) { if (!meetingId || !userId) { return NextResponse.json({ message: 'Missing required parameters' }, { status: 400 }); } + + // Verify that the authenticated user matches the requested userId + if (userId !== authenticatedUserId) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Cannot access other user\'s notes' + }, { status: 403 }); + } const notes = await MeetingNotes.findOne({ meetingId, userId }); return NextResponse.json(notes || { content: '', title: 'Meeting Notes' }); @@ -23,11 +42,29 @@ export async function GET(req: NextRequest) { export async function POST(req: NextRequest) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req); + if (!authResult.isValid) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Invalid or missing token' + }, { status: 401 }); + } + + const authenticatedUserId = authResult.userId; const { meetingId, userId, userName, content, title, tags, isPrivate } = await req.json(); if (!meetingId || !userId || !userName) { return NextResponse.json({ message: 'Missing required fields' }, { status: 400 }); } + + // Verify that the authenticated user matches the userId in the request + if (userId !== authenticatedUserId) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Cannot create notes for other users' + }, { status: 403 }); + } // Calculate word count const wordCount = content ? content.trim().split(/\s+/).filter((word: string) => word.length > 0).length : 0; @@ -63,6 +100,16 @@ export async function POST(req: NextRequest) { export async function DELETE(req: NextRequest) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req); + if (!authResult.isValid) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Invalid or missing token' + }, { status: 401 }); + } + + const authenticatedUserId = authResult.userId; const url = new URL(req.url); const meetingId = url.searchParams.get('meetingId'); const userId = url.searchParams.get('userId'); @@ -70,6 +117,14 @@ export async function DELETE(req: NextRequest) { if (!meetingId || !userId) { return NextResponse.json({ message: 'Missing required parameters' }, { status: 400 }); } + + // Verify that the authenticated user matches the userId + if (userId !== authenticatedUserId) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Cannot delete other user\'s notes' + }, { status: 403 }); + } await MeetingNotes.findOneAndDelete({ meetingId, userId }); return NextResponse.json({ message: 'Notes deleted successfully' }); diff --git a/src/app/api/meeting-notes/user/route.ts b/src/app/api/meeting-notes/user/route.ts index 92d26a91..cfc1541f 100644 --- a/src/app/api/meeting-notes/user/route.ts +++ b/src/app/api/meeting-notes/user/route.ts @@ -2,10 +2,21 @@ import { NextRequest, NextResponse } from 'next/server'; import connect from '@/lib/db'; import MeetingNotes from '@/lib/models/meetingNotesSchema'; import Meeting from '@/lib/models/meetingSchema'; +import { validateAndExtractUserId } from '@/utils/jwtAuth'; export async function GET(req: NextRequest) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req); + if (!authResult.isValid) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Invalid or missing token' + }, { status: 401 }); + } + + const authenticatedUserId = authResult.userId; const url = new URL(req.url); const userId = url.searchParams.get('userId'); const otherUserId = url.searchParams.get('otherUserId'); @@ -13,6 +24,14 @@ export async function GET(req: NextRequest) { if (!userId) { return NextResponse.json({ message: 'Missing userId parameter' }, { status: 400 }); } + + // Verify that the authenticated user matches the requested userId + if (userId !== authenticatedUserId) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Cannot access other user\'s meeting notes' + }, { status: 403 }); + } let query: any = { userId }; diff --git a/src/app/api/meeting/[id]/route.ts b/src/app/api/meeting/[id]/route.ts index 4d20c897..c445e8c5 100644 --- a/src/app/api/meeting/[id]/route.ts +++ b/src/app/api/meeting/[id]/route.ts @@ -1,11 +1,18 @@ import meetingSchema from "@/lib/models/meetingSchema"; -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import connect from "@/lib/db"; +import { validateAndExtractUserId } from "@/utils/jwtAuth"; -export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) { +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { await connect(); try { + // Validate authentication + const authenticatedUserId = await validateAndExtractUserId(req); + if (!authenticatedUserId) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const { id } = await params; if (!id) { @@ -18,6 +25,11 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri return NextResponse.json({ message: "Meeting not found" }, { status: 404 }); } + // Verify user authorization - only participants can access meeting details + if (meeting.senderId.toString() !== authenticatedUserId.userId && meeting.receiverId.toString() !== authenticatedUserId.userId) { + return NextResponse.json({ message: "Unauthorized to access this meeting" }, { status: 403 }); + } + return NextResponse.json(meeting, { status: 200 }); } catch (error: any) { console.error('Error fetching meeting:', error); @@ -25,10 +37,16 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri } } -export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) { +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { await connect(); try { + // Validate authentication + const authenticatedUserId = await validateAndExtractUserId(req); + if (!authenticatedUserId) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const { id } = await params; const updateData = await req.json(); @@ -42,6 +60,11 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st return NextResponse.json({ message: "Meeting not found" }, { status: 404 }); } + // Verify user authorization - only participants can update meetings + if (meeting.senderId.toString() !== authenticatedUserId.userId && meeting.receiverId.toString() !== authenticatedUserId.userId) { + return NextResponse.json({ message: "Unauthorized to update this meeting" }, { status: 403 }); + } + // Update the meeting with the provided data Object.assign(meeting, updateData); const updatedMeeting = await meeting.save(); diff --git a/src/app/api/meeting/cancel/route.ts b/src/app/api/meeting/cancel/route.ts index 79e30f63..f6f6f8b9 100644 --- a/src/app/api/meeting/cancel/route.ts +++ b/src/app/api/meeting/cancel/route.ts @@ -1,12 +1,19 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import connect from "@/lib/db"; import meetingSchema from "@/lib/models/meetingSchema"; import cancelMeetingSchema from "@/lib/models/cancelMeetingSchema"; +import { validateAndExtractUserId } from "@/utils/jwtAuth"; -export async function POST(req: Request) { +export async function POST(req: NextRequest) { await connect(); try { + // Validate authentication + const authResult = await validateAndExtractUserId(req); + if (!authResult) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const { meetingId, cancelledBy, reason } = await req.json(); // Validate input @@ -17,6 +24,14 @@ export async function POST(req: Request) { ); } + // Verify the cancelling user is the authenticated user + if (cancelledBy !== authResult.userId) { + return NextResponse.json( + { message: "Cannot cancel meeting on behalf of another user" }, + { status: 403 } + ); + } + // Find the meeting const meeting = await meetingSchema.findById(meetingId); if (!meeting) { @@ -26,6 +41,14 @@ export async function POST(req: Request) { ); } + // Verify user authorization - only participants can cancel meetings + if (meeting.senderId.toString() !== authResult.userId && meeting.receiverId.toString() !== authResult.userId) { + return NextResponse.json( + { message: "Unauthorized to cancel this meeting" }, + { status: 403 } + ); + } + // Check if meeting can be cancelled if (meeting.state === 'cancelled' || meeting.state === 'rejected') { return NextResponse.json( diff --git a/src/app/api/meeting/cancellation/acknowledge/route.ts b/src/app/api/meeting/cancellation/acknowledge/route.ts index f480e824..31b2fb3b 100644 --- a/src/app/api/meeting/cancellation/acknowledge/route.ts +++ b/src/app/api/meeting/cancellation/acknowledge/route.ts @@ -1,27 +1,45 @@ import { NextRequest, NextResponse } from 'next/server'; import connect from '@/lib/db'; import cancelMeetingSchema from '@/lib/models/cancelMeetingSchema'; +import { validateAndExtractUserId } from '@/utils/jwtAuth'; export async function POST(request: NextRequest) { try { await connect(); - const { meetingId, userId } = await request.json(); + // Validate authentication + const userId = await validateAndExtractUserId(request); + if (!userId) { + return NextResponse.json( + { success: false, message: "Authentication required" }, + { status: 401 } + ); + } + + const { meetingId, userId: requestUserId } = await request.json(); - if (!meetingId || !userId) { + if (!meetingId || !requestUserId) { return NextResponse.json( { success: false, message: 'Meeting ID and User ID are required' }, { status: 400 } ); } + // Verify the user can only acknowledge for themselves + if (requestUserId !== userId) { + return NextResponse.json( + { success: false, message: 'Cannot acknowledge cancellation for another user' }, + { status: 403 } + ); + } + // Find the cancellation record and mark it as acknowledged const cancellation = await cancelMeetingSchema.findOneAndUpdate( { meetingId }, { acknowledged: true, acknowledgedAt: new Date(), - acknowledgedBy: userId + acknowledgedBy: requestUserId }, { new: true } ); diff --git a/src/app/api/meeting/cancellation/route.ts b/src/app/api/meeting/cancellation/route.ts index d34d08be..699d09ba 100644 --- a/src/app/api/meeting/cancellation/route.ts +++ b/src/app/api/meeting/cancellation/route.ts @@ -1,12 +1,19 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import connect from "@/lib/db"; import cancelMeetingSchema from "@/lib/models/cancelMeetingSchema"; +import { validateAndExtractUserId } from "@/utils/jwtAuth"; // Get cancellation details -export async function GET(req: Request) { +export async function GET(req: NextRequest) { await connect(); try { + // Validate authentication + const authenticatedUserId = await validateAndExtractUserId(req); + if (!authenticatedUserId) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const url = new URL(req.url); const meetingId = url.searchParams.get('meetingId'); const userId = url.searchParams.get('userId'); @@ -19,6 +26,21 @@ export async function GET(req: Request) { ); } + // Verify the user can only access their own cancellation data + if (userId && userId !== authenticatedUserId.userId) { + return NextResponse.json( + { message: "Cannot access cancellation data for another user" }, + { status: 403 } + ); + } + + if (!meetingId) { + return NextResponse.json( + { message: "Meeting ID is required" }, + { status: 400 } + ); + } + let query: any = { meetingId }; // If userId is provided and includeAcknowledged is false, filter out acknowledged cancellations for that user @@ -43,10 +65,16 @@ export async function GET(req: Request) { } // Acknowledge cancellation -export async function PATCH(req: Request) { +export async function PATCH(req: NextRequest) { await connect(); try { + // Validate authentication + const authenticatedUserId = await validateAndExtractUserId(req); + if (!authenticatedUserId) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const { cancellationId, acknowledgedBy } = await req.json(); if (!cancellationId || !acknowledgedBy) { @@ -56,6 +84,14 @@ export async function PATCH(req: Request) { ); } + // Verify the user can only acknowledge for themselves + if (acknowledgedBy !== authenticatedUserId.userId) { + return NextResponse.json( + { message: "Cannot acknowledge cancellation for another user" }, + { status: 403 } + ); + } + const cancellation = await cancelMeetingSchema.findById(cancellationId); if (!cancellation) { diff --git a/src/app/api/meeting/cancellation/unacknowledged/route.ts b/src/app/api/meeting/cancellation/unacknowledged/route.ts index 135aebbe..dbaed1b3 100644 --- a/src/app/api/meeting/cancellation/unacknowledged/route.ts +++ b/src/app/api/meeting/cancellation/unacknowledged/route.ts @@ -1,14 +1,21 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import connect from "@/lib/db"; import cancelMeetingSchema from "@/lib/models/cancelMeetingSchema"; import meetingSchema from "@/lib/models/meetingSchema"; import userSchema from "@/lib/models/userSchema"; +import { validateAndExtractUserId } from "@/utils/jwtAuth"; // Get all unacknowledged cancellations for a user -export async function GET(req: Request) { +export async function GET(req: NextRequest) { await connect(); try { + // Validate authentication + const authenticatedUserId = await validateAndExtractUserId(req); + if (!authenticatedUserId) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const url = new URL(req.url); const userId = url.searchParams.get('userId'); @@ -19,6 +26,14 @@ export async function GET(req: Request) { ); } + // Verify the user can only access their own unacknowledged cancellations + if (userId !== authenticatedUserId.userId) { + return NextResponse.json( + { message: "Cannot access unacknowledged cancellations for another user" }, + { status: 403 } + ); + } + // Find all meetings involving this user const userMeetings = await meetingSchema.find({ $or: [ diff --git a/src/app/api/meeting/reject/route.ts b/src/app/api/meeting/reject/route.ts index 76705dcd..955b778c 100644 --- a/src/app/api/meeting/reject/route.ts +++ b/src/app/api/meeting/reject/route.ts @@ -1,10 +1,17 @@ import meetingSchema from "@/lib/models/meetingSchema"; -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import connect from "@/lib/db"; +import { validateAndExtractUserId } from "@/utils/jwtAuth"; -export async function POST(req: Request) { +export async function POST(req: NextRequest) { await connect(); try { + // Validate authentication + const authResult = await validateAndExtractUserId(req); + if (!authResult) { + return NextResponse.json({ message: "Authentication required" }, { status: 401 }); + } + const { meetingId } = await req.json(); if (!meetingId) { @@ -17,6 +24,11 @@ export async function POST(req: Request) { return NextResponse.json({ message: "Meeting not found" }, { status: 404 }); } + // Verify user authorization - only participants can reject meetings + if (meeting.senderId.toString() !== authResult.userId && meeting.receiverId.toString() !== authResult.userId) { + return NextResponse.json({ message: "Unauthorized to reject this meeting" }, { status: 403 }); + } + // ! Reject meeting.state = "rejected"; const updatedMeeting = await meeting.save(); diff --git a/src/app/api/meeting/route.ts b/src/app/api/meeting/route.ts index a9f3e0ae..7775c965 100644 --- a/src/app/api/meeting/route.ts +++ b/src/app/api/meeting/route.ts @@ -1,6 +1,7 @@ import meetingSchema from "@/lib/models/meetingSchema"; import { NextResponse } from "next/server"; import connect from "@/lib/db"; +import { validateAndExtractUserId } from '@/utils/jwtAuth'; // Daily.co configuration const DAILY_API_KEY = process.env.DAILY_API_KEY || "30a32b5fc8651595f2b981d1210cdd8b9e5b9caececb714da81b825a18f6aa11"; @@ -99,10 +100,28 @@ const testDailyAPI = async () => { export async function GET(req: Request) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req as any); + if (!authResult.isValid) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Invalid or missing token' + }, { status: 401 }); + } + + const authenticatedUserId = authResult.userId; const url = new URL(req.url); const userId = url.searchParams.get('userId'); const otherUserId = url.searchParams.get('otherUserId'); + // Verify that the authenticated user matches the requested userId + if (userId && userId !== authenticatedUserId) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Cannot access other user\'s meetings' + }, { status: 403 }); + } + let query = {}; if (userId && otherUserId) { // Fetch meetings between two specific users @@ -120,8 +139,15 @@ export async function GET(req: Request) { { receiverId: userId } ] }; + } else { + // If no userId provided, return meetings for authenticated user + query = { + $or: [ + { senderId: authenticatedUserId }, + { receiverId: authenticatedUserId } + ] + }; } - // If no userId is provided, return empty array (don't return all meetings) const meetings = await meetingSchema.find(query); return NextResponse.json(meetings, { status: 200 }); @@ -133,8 +159,26 @@ export async function GET(req: Request) { export async function POST(req: Request) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req as any); + if (!authResult.isValid) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Invalid or missing token' + }, { status: 401 }); + } + + const authenticatedUserId = authResult.userId; const body = await req.json(); // Parse the JSON body first + // Verify that the authenticated user is the sender + if (body.senderId !== authenticatedUserId) { + return NextResponse.json({ + success: false, + message: 'Unauthorized - Cannot create meetings as another user' + }, { status: 403 }); + } + // Check if users already have 2 active meetings const existingMeetings = await meetingSchema.find({ $or: [ diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts index 84b2cdad..96e5b118 100644 --- a/src/app/api/messages/route.ts +++ b/src/app/api/messages/route.ts @@ -4,6 +4,7 @@ import Message from "@/lib/models/messageSchema"; import ChatRoom from "@/lib/models/chatRoomSchema"; import mongoose from "mongoose"; import { encryptMessage, decryptMessage } from "@/lib/messageEncryption/encryption"; +import { validateAndExtractUserId } from "@/utils/jwtAuth"; /** ** POST handler - Creates a new message in a chat room @@ -20,11 +21,29 @@ import { encryptMessage, decryptMessage } from "@/lib/messageEncryption/encrypti export async function POST(req: Request) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req as any); + if (!authResult.isValid) { + return NextResponse.json( + { success: false, message: "Unauthorized - Invalid or missing token" }, + { status: 401 } + ); + } + + const authenticatedUserId = authResult.userId; const body = await req.json(); //console.log("Received body:", body); const { chatRoomId, senderId, content, replyFor } = body; + // Verify that the authenticated user matches the senderId + if (senderId !== authenticatedUserId) { + return NextResponse.json( + { success: false, message: "Unauthorized - Cannot send messages as another user" }, + { status: 403 } + ); + } + // Check if content is a file link and skip encryption if it is const isFileLink = content.startsWith('File:'); @@ -57,7 +76,14 @@ export async function POST(req: Request) { { success: false, message: "Chat room not found" }, { status: 404 } ); - + } + + // Verify that the authenticated user is a participant in the chat room + if (!chatRoom.participants.includes(authenticatedUserId)) { + return NextResponse.json( + { success: false, message: "Unauthorized - User is not a participant in this chat room" }, + { status: 403 } + ); } const message = await Message.create({ @@ -122,6 +148,16 @@ export async function POST(req: Request) { export async function GET(req: Request) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req as any); + if (!authResult.isValid) { + return NextResponse.json( + { success: false, message: "Unauthorized - Invalid or missing token" }, + { status: 401 } + ); + } + + const authenticatedUserId = authResult.userId; const { searchParams } = new URL(req.url); const chatRoomId = searchParams.get("chatRoomId"); const lastMessageOnly = searchParams.get("lastMessage") === "true"; @@ -135,6 +171,22 @@ export async function GET(req: Request) { const chatRoomObjectId = new mongoose.Types.ObjectId(chatRoomId); + // Verify that the authenticated user is a participant in the chat room + const chatRoom = await ChatRoom.findById(chatRoomObjectId); + if (!chatRoom) { + return NextResponse.json( + { success: false, message: "Chat room not found" }, + { status: 404 } + ); + } + + if (!chatRoom.participants.includes(authenticatedUserId)) { + return NextResponse.json( + { success: false, message: "Unauthorized - User is not a participant in this chat room" }, + { status: 403 } + ); + } + if (lastMessageOnly) { const lastMessage = await Message.findOne({ chatRoomId: chatRoomObjectId }) .sort({ sentAt: -1 }) @@ -204,6 +256,16 @@ export async function GET(req: Request) { export async function PATCH(req: Request) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req as any); + if (!authResult.isValid) { + return NextResponse.json( + { success: false, message: "Unauthorized - Invalid or missing token" }, + { status: 401 } + ); + } + + const authenticatedUserId = authResult.userId; const body = await req.json(); const { messageId } = body; @@ -222,6 +284,15 @@ export async function PATCH(req: Request) { ); } + // Verify that the authenticated user is a participant in the chat room + const chatRoom = await ChatRoom.findById(existingMessage.chatRoomId); + if (!chatRoom || !chatRoom.participants.includes(authenticatedUserId)) { + return NextResponse.json( + { success: false, message: "Unauthorized - User is not a participant in this chat room" }, + { status: 403 } + ); + } + if (existingMessage.readStatus) { return NextResponse.json( { success: true, message: "Message already read", updatedMessage: existingMessage }, @@ -260,6 +331,16 @@ export async function PATCH(req: Request) { export async function DELETE(req: Request) { await connect(); try { + // Authenticate user first + const authResult = validateAndExtractUserId(req as any); + if (!authResult.isValid) { + return NextResponse.json( + { success: false, message: "Unauthorized - Invalid or missing token" }, + { status: 401 } + ); + } + + const authenticatedUserId = authResult.userId; const { searchParams } = new URL(req.url); const messageId = searchParams.get("messageId"); @@ -278,6 +359,23 @@ export async function DELETE(req: Request) { ); } + // Verify that the authenticated user is either the sender or a participant in the chat room + const chatRoom = await ChatRoom.findById(existingMessage.chatRoomId); + if (!chatRoom || !chatRoom.participants.includes(authenticatedUserId)) { + return NextResponse.json( + { success: false, message: "Unauthorized - User is not a participant in this chat room" }, + { status: 403 } + ); + } + + // Additional check: Only the message sender can delete their own messages + if (existingMessage.senderId !== authenticatedUserId) { + return NextResponse.json( + { success: false, message: "Unauthorized - Users can only delete their own messages" }, + { status: 403 } + ); + } + await Message.findByIdAndDelete(messageId); return NextResponse.json( diff --git a/src/components/messageSystem/MessageInput.tsx b/src/components/messageSystem/MessageInput.tsx index d23844f0..9cd57581 100644 --- a/src/components/messageSystem/MessageInput.tsx +++ b/src/components/messageSystem/MessageInput.tsx @@ -7,6 +7,16 @@ import { IMessage } from "@/types/chat"; import { sendMessage as sendMessageService, fetchUserProfile } from "@/services/chatApiServices"; +/** + * Helper function to get authentication headers for file uploads + */ +function getAuthHeaders(): HeadersInit { + const token = localStorage.getItem('auth_token'); + return { + ...(token && { 'Authorization': `Bearer ${token}` }) + }; +} + interface MessageInputProps { chatRoomId: string; senderId: string; @@ -100,6 +110,7 @@ export default function MessageInput({ try { const response = await fetch("/api/file/upload", { method: "POST", + headers: getAuthHeaders(), body: formData, }); diff --git a/src/services/chatApiServices.ts b/src/services/chatApiServices.ts index d15f93d0..03825096 100644 --- a/src/services/chatApiServices.ts +++ b/src/services/chatApiServices.ts @@ -30,6 +30,17 @@ interface OnlineLogResponse { }; } +/** + * Helper function to get authentication headers + */ +function getAuthHeaders(): HeadersInit { + const token = localStorage.getItem('auth_token'); + return { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }) + }; +} + /** ** Fetch a chat room by chatRoomId * @@ -227,7 +238,7 @@ export async function sendMessage(messageData: any) { try { const response = await fetch("/api/messages", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: getAuthHeaders(), body: JSON.stringify(messageData), }); @@ -252,7 +263,7 @@ export async function sendMessage(messageData: any) { // ! Create notification await fetch("/api/notification", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: getAuthHeaders(), body: JSON.stringify({ userId: recipientId, typeno: 2, // Type 2 for new message notification @@ -279,7 +290,10 @@ export async function sendMessage(messageData: any) { */ export async function fetchChatMessages(chatRoomId: string) { try { - const response = await fetch(`/api/messages?chatRoomId=${chatRoomId}`); + const response = await fetch(`/api/messages?chatRoomId=${chatRoomId}`, { + method: "GET", + headers: getAuthHeaders(), + }); const data = await response.json(); if (data.success) { @@ -303,7 +317,7 @@ export async function markMessagesAsRead(messageIds: string[]) { try { const response = await fetch("/api/messages/read-status", { method: "PATCH", - headers: { "Content-Type": "application/json" }, + headers: getAuthHeaders(), body: JSON.stringify({ messageIds }), }); @@ -322,7 +336,10 @@ export async function markMessagesAsRead(messageIds: string[]) { */ export async function fetchUnreadMessageCount(userId: string) { try { - const response = await fetch(`/api/messages/unread-count?userId=${userId}`); + const response = await fetch(`/api/messages/unread-count?userId=${userId}`, { + method: "GET", + headers: getAuthHeaders(), + }); const data = await response.json(); if (data.success) { @@ -344,7 +361,10 @@ export async function fetchUnreadMessageCount(userId: string) { */ export async function fetchUnreadMessageCountsByRoom(userId: string) { try { - const response = await fetch(`/api/messages/unread-by-room?userId=${userId}`); + const response = await fetch(`/api/messages/unread-by-room?userId=${userId}`, { + method: "GET", + headers: getAuthHeaders(), + }); const data = await response.json(); if (data.success) { diff --git a/src/services/meetingApiServices.ts b/src/services/meetingApiServices.ts index 3833c9eb..f55d1310 100644 --- a/src/services/meetingApiServices.ts +++ b/src/services/meetingApiServices.ts @@ -2,6 +2,17 @@ import Meeting from "@/types/meeting"; import { debouncedApiService } from './debouncedApiService'; import { invalidateMeetingCache, invalidateUsersCaches } from './sessionApiServices'; +/** + * Helper function to get authentication headers + */ +function getAuthHeaders(): HeadersInit { + const token = localStorage.getItem('auth_token'); + return { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }) + }; +} + // Notification helper functions /** * Send meeting notification to a user @@ -15,9 +26,7 @@ async function sendMeetingNotification(userId: string, typeno: number, descripti try { const response = await fetch('/api/notification', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: getAuthHeaders(), body: JSON.stringify({ userId, typeno, @@ -46,7 +55,10 @@ async function sendMeetingNotification(userId: string, typeno: number, descripti */ async function getUserName(userId: string): Promise { try { - const response = await fetch(`/api/users/profile?id=${userId}`); + const response = await fetch(`/api/users/profile?id=${userId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); const data = await response.json(); if (data.success && data.user) { @@ -74,7 +86,10 @@ export async function fetchMeetings(userId: string, otherUserId: string): Promis return debouncedApiService.makeRequest( cacheKey, async () => { - const response = await fetch(`/api/meeting?userId=${userId}&otherUserId=${otherUserId}`); + const response = await fetch(`/api/meeting?userId=${userId}&otherUserId=${otherUserId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); if (!response.ok) { throw new Error(`Error fetching meetings: ${response.status}`); @@ -106,7 +121,7 @@ export async function createMeeting(meetingData: { try { const response = await fetch('/api/meeting', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify(meetingData), }); @@ -165,7 +180,7 @@ export async function updateMeeting(meetingId: string, action: 'accept' | 'rejec if (action === 'reject') { response = await fetch('/api/meeting/reject', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify({ meetingId }), }); } else { @@ -180,7 +195,7 @@ export async function updateMeeting(meetingId: string, action: 'accept' | 'rejec response = await fetch('/api/meeting', { method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify(body), }); } @@ -273,7 +288,10 @@ export const fetchAllUserMeetings = async (userId: string): Promise = return debouncedApiService.makeRequest( cacheKey, async () => { - const response = await fetch(`/api/meeting?userId=${userId}`); + const response = await fetch(`/api/meeting?userId=${userId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); @@ -305,7 +323,7 @@ export async function cancelMeetingWithReason( const url = '/api/meeting/cancel'; const response = await fetch(url, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify({ meetingId, cancelledBy, @@ -347,7 +365,10 @@ export async function fetchMeetingCancellation(meetingId: string, userId: string return debouncedApiService.makeRequest( cacheKey, async () => { - const response = await fetch(`/api/meeting/cancellation?meetingId=${meetingId}&userId=${userId}`); + const response = await fetch(`/api/meeting/cancellation?meetingId=${meetingId}&userId=${userId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); if (!response.ok) return null; return await response.json(); }, @@ -367,7 +388,10 @@ export async function fetchUnacknowledgedCancellations(userId: string) { return debouncedApiService.makeRequest( cacheKey, async () => { - const response = await fetch(`/api/meeting/cancellation/unacknowledged?userId=${userId}`); + const response = await fetch(`/api/meeting/cancellation/unacknowledged?userId=${userId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); if (!response.ok) return []; return await response.json(); }, @@ -389,7 +413,7 @@ export async function acknowledgeMeetingCancellation( try { const response = await fetch('/api/meeting/cancellation', { method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + headers: getAuthHeaders(), body: JSON.stringify({ cancellationId, acknowledgedBy @@ -423,7 +447,10 @@ export async function checkMeetingNotesExist(meetingId: string, userId: string): return debouncedApiService.makeRequest( cacheKey, async () => { - const response = await fetch(`/api/meeting-notes?meetingId=${meetingId}&userId=${userId}`); + const response = await fetch(`/api/meeting-notes?meetingId=${meetingId}&userId=${userId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); const data = await response.json(); return response.ok && data._id && data.content && data.content.trim().length > 0; }, @@ -440,7 +467,10 @@ export async function checkMeetingNotesExist(meetingId: string, userId: string): */ export async function fetchMeetingNotes(meetingId: string, userId: string) { try { - const response = await fetch(`/api/meeting-notes?meetingId=${meetingId}&userId=${userId}`); + const response = await fetch(`/api/meeting-notes?meetingId=${meetingId}&userId=${userId}`, { + method: 'GET', + headers: getAuthHeaders(), + }); const data = await response.json(); if (response.ok && data._id && data.content && data.content.trim().length > 0) { @@ -468,7 +498,10 @@ export async function fetchAllUserMeetingNotes(userId: string, otherUserId?: str cacheKey, async () => { const url = `/api/meeting-notes/user?userId=${userId}${otherUserId ? `&otherUserId=${otherUserId}` : ''}`; - const response = await fetch(url); + const response = await fetch(url, { + method: 'GET', + headers: getAuthHeaders(), + }); if (!response.ok) { throw new Error(`Error fetching user meeting notes: ${response.status}`);