diff --git a/maternal-web/app/api/ai/chat/route.ts b/maternal-web/app/api/ai/chat/route.ts new file mode 100644 index 0000000..0b7dbc7 --- /dev/null +++ b/maternal-web/app/api/ai/chat/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { aiLimiter } from '@/lib/middleware/rateLimiter'; + +/** + * AI chat endpoint with rate limiting + * Limited to 10 queries per hour for free tier users + */ +export async function POST(request: NextRequest) { + // Apply rate limiting + const rateLimitResult = await aiLimiter(request); + if (rateLimitResult) return rateLimitResult; + + try { + const body = await request.json(); + const { message, childId, conversationId } = body; + + // TODO: Implement actual AI chat logic + // This is a placeholder - actual AI integration will be handled by backend + + // For now, forward to backend API + const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; + const response = await fetch(`${backendUrl}/api/v1/ai/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Forward auth token from client + Authorization: request.headers.get('Authorization') || '', + }, + body: JSON.stringify({ message, childId, conversationId }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('[AI] Chat error:', error); + return NextResponse.json( + { + error: 'AI_CHAT_FAILED', + message: 'AI assistant is currently unavailable. Please try again later.', + }, + { status: 500 } + ); + } +} diff --git a/maternal-web/app/api/auth/login/route.ts b/maternal-web/app/api/auth/login/route.ts new file mode 100644 index 0000000..9401127 --- /dev/null +++ b/maternal-web/app/api/auth/login/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { authLimiter } from '@/lib/middleware/rateLimiter'; + +/** + * Login endpoint with rate limiting + * Limited to 5 attempts per 15 minutes per IP + */ +export async function POST(request: NextRequest) { + // Apply rate limiting + const rateLimitResult = await authLimiter(request); + if (rateLimitResult) return rateLimitResult; + + try { + const body = await request.json(); + const { email, password } = body; + + // TODO: Implement actual authentication logic + // This is a placeholder - actual auth will be handled by backend + + // For now, forward to backend API + const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; + const response = await fetch(`${backendUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('[Auth] Login error:', error); + return NextResponse.json( + { + error: 'AUTH_LOGIN_FAILED', + message: 'Login failed. Please try again.', + }, + { status: 500 } + ); + } +} diff --git a/maternal-web/app/api/auth/password-reset/route.ts b/maternal-web/app/api/auth/password-reset/route.ts new file mode 100644 index 0000000..8c50282 --- /dev/null +++ b/maternal-web/app/api/auth/password-reset/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { authLimiter } from '@/lib/middleware/rateLimiter'; + +/** + * Password reset request endpoint with rate limiting + * Limited to 5 attempts per 15 minutes per IP + */ +export async function POST(request: NextRequest) { + // Apply rate limiting + const rateLimitResult = await authLimiter(request); + if (rateLimitResult) return rateLimitResult; + + try { + const body = await request.json(); + const { email } = body; + + // TODO: Implement actual password reset logic + // This is a placeholder - actual password reset will be handled by backend + + // For now, forward to backend API + const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; + const response = await fetch(`${backendUrl}/api/v1/auth/password-reset`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('[Auth] Password reset error:', error); + return NextResponse.json( + { + error: 'AUTH_PASSWORD_RESET_FAILED', + message: 'Password reset request failed. Please try again.', + }, + { status: 500 } + ); + } +} diff --git a/maternal-web/app/api/auth/register/route.ts b/maternal-web/app/api/auth/register/route.ts new file mode 100644 index 0000000..2e9ba47 --- /dev/null +++ b/maternal-web/app/api/auth/register/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { authLimiter } from '@/lib/middleware/rateLimiter'; + +/** + * Registration endpoint with rate limiting + * Limited to 5 attempts per 15 minutes per IP + */ +export async function POST(request: NextRequest) { + // Apply rate limiting + const rateLimitResult = await authLimiter(request); + if (rateLimitResult) return rateLimitResult; + + try { + const body = await request.json(); + const { email, password, name } = body; + + // TODO: Implement actual registration logic + // This is a placeholder - actual registration will be handled by backend + + // For now, forward to backend API + const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; + const response = await fetch(`${backendUrl}/api/v1/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password, name }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: 201 }); + } catch (error) { + console.error('[Auth] Registration error:', error); + return NextResponse.json( + { + error: 'AUTH_REGISTRATION_FAILED', + message: 'Registration failed. Please try again.', + }, + { status: 500 } + ); + } +} diff --git a/maternal-web/app/api/tracking/feeding/route.ts b/maternal-web/app/api/tracking/feeding/route.ts new file mode 100644 index 0000000..c92421d --- /dev/null +++ b/maternal-web/app/api/tracking/feeding/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { trackingLimiter } from '@/lib/middleware/rateLimiter'; + +/** + * Feeding tracking endpoint with rate limiting + * Limited to 30 requests per minute (reasonable for frequent tracking) + */ +export async function POST(request: NextRequest) { + // Apply rate limiting + const rateLimitResult = await trackingLimiter(request); + if (rateLimitResult) return rateLimitResult; + + try { + const body = await request.json(); + const { childId, type, amount, unit, startTime, endTime, notes } = body; + + // TODO: Implement actual tracking logic + // This is a placeholder - actual tracking will be handled by backend + + // For now, forward to backend API + const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; + const response = await fetch(`${backendUrl}/api/v1/tracking/feeding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Forward auth token from client + Authorization: request.headers.get('Authorization') || '', + }, + body: JSON.stringify({ childId, type, amount, unit, startTime, endTime, notes }), + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: 201 }); + } catch (error) { + console.error('[Tracking] Feeding error:', error); + return NextResponse.json( + { + error: 'TRACKING_FEEDING_FAILED', + message: 'Failed to save feeding record. Please try again.', + }, + { status: 500 } + ); + } +} + +/** + * Get feeding history with rate limiting + */ +export async function GET(request: NextRequest) { + // Apply rate limiting + const rateLimitResult = await trackingLimiter(request); + if (rateLimitResult) return rateLimitResult; + + try { + const { searchParams } = new URL(request.url); + const childId = searchParams.get('childId'); + const startDate = searchParams.get('startDate'); + const endDate = searchParams.get('endDate'); + + // For now, forward to backend API + const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; + const queryString = new URLSearchParams({ + ...(childId && { childId }), + ...(startDate && { startDate }), + ...(endDate && { endDate }), + }).toString(); + + const response = await fetch(`${backendUrl}/api/v1/tracking/feeding?${queryString}`, { + method: 'GET', + headers: { + // Forward auth token from client + Authorization: request.headers.get('Authorization') || '', + }, + }); + + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('[Tracking] Get feeding history error:', error); + return NextResponse.json( + { + error: 'TRACKING_FEEDING_GET_FAILED', + message: 'Failed to retrieve feeding history. Please try again.', + }, + { status: 500 } + ); + } +} diff --git a/maternal-web/lib/middleware/rateLimiter.ts b/maternal-web/lib/middleware/rateLimiter.ts new file mode 100644 index 0000000..87d279d --- /dev/null +++ b/maternal-web/lib/middleware/rateLimiter.ts @@ -0,0 +1,172 @@ +/** + * Rate limiting for Next.js API routes + * Uses in-memory store (for production, use Redis) + */ + +interface RateLimitEntry { + count: number; + resetTime: number; +} + +// In-memory store for rate limiting +const rateLimitStore = new Map(); + +// Cleanup old entries every 5 minutes +setInterval(() => { + const now = Date.now(); + for (const [key, entry] of rateLimitStore.entries()) { + if (entry.resetTime < now) { + rateLimitStore.delete(key); + } + } +}, 5 * 60 * 1000); + +interface RateLimitConfig { + windowMs: number; // Time window in milliseconds + max: number; // Max requests per window + message: { + error: string; + message: string; + retryAfter: number; // seconds + }; + keyGenerator?: (request: Request) => string; + skipSuccessfulRequests?: boolean; +} + +/** + * Creates a rate limiter middleware + */ +export function createRateLimiter(config: RateLimitConfig) { + return async (request: Request): Promise => { + const now = Date.now(); + const key = config.keyGenerator + ? config.keyGenerator(request) + : request.headers.get('x-forwarded-for') || 'unknown'; + + const identifier = `${key}:${request.url}`; + const entry = rateLimitStore.get(identifier); + + if (!entry || entry.resetTime < now) { + // Create new entry + rateLimitStore.set(identifier, { + count: 1, + resetTime: now + config.windowMs, + }); + + return null; // Allow request + } + + if (entry.count >= config.max) { + // Rate limit exceeded + const retryAfter = Math.ceil((entry.resetTime - now) / 1000); + + return new Response( + JSON.stringify({ + ...config.message, + retryAfter, + }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + 'RateLimit-Limit': config.max.toString(), + 'RateLimit-Remaining': '0', + 'RateLimit-Reset': entry.resetTime.toString(), + 'Retry-After': retryAfter.toString(), + }, + } + ); + } + + // Increment count + entry.count += 1; + rateLimitStore.set(identifier, entry); + + return null; // Allow request + }; +} + +/** + * Strict rate limit for authentication endpoints (login, register, password reset) + */ +export const authLimiter = createRateLimiter({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // 5 attempts per 15 minutes + message: { + error: 'AUTH_RATE_LIMIT_EXCEEDED', + message: 'Too many authentication attempts. Please try again later.', + retryAfter: 15 * 60, + }, +}); + +/** + * Moderate rate limit for AI assistant endpoints + */ +export const aiLimiter = createRateLimiter({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 10, // 10 queries per hour + message: { + error: 'AI_RATE_LIMIT_EXCEEDED', + message: 'You have reached your AI assistant query limit. Please try again later or upgrade to premium.', + retryAfter: 60 * 60, + }, + keyGenerator: (req) => { + // TODO: Use user ID from session/token when available + return req.headers.get('x-forwarded-for') || 'unknown'; + }, +}); + +/** + * General rate limit for tracking endpoints (feeding, sleep, etc.) + */ +export const trackingLimiter = createRateLimiter({ + windowMs: 1 * 60 * 1000, // 1 minute + max: 30, // 30 requests per minute + message: { + error: 'TRACKING_RATE_LIMIT_EXCEEDED', + message: 'Too many tracking requests. Please slow down.', + retryAfter: 60, + }, +}); + +/** + * Lenient rate limit for read-only endpoints (GET requests) + */ +export const readLimiter = createRateLimiter({ + windowMs: 1 * 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + message: { + error: 'READ_RATE_LIMIT_EXCEEDED', + message: 'Too many requests. Please slow down.', + retryAfter: 60, + }, +}); + +/** + * Very strict rate limit for sensitive operations (delete account, etc.) + */ +export const sensitiveLimiter = createRateLimiter({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 3, // Only 3 sensitive operations per hour + message: { + error: 'SENSITIVE_RATE_LIMIT_EXCEEDED', + message: 'Too many sensitive operations. Please try again later.', + retryAfter: 60 * 60, + }, +}); + +/** + * Helper to apply rate limiter to Next.js API routes + * + * Usage: + * ```typescript + * import { authLimiter } from '@/lib/middleware/rateLimiter'; + * + * export async function POST(request: Request) { + * const rateLimitResult = await authLimiter(request); + * if (rateLimitResult) return rateLimitResult; // Return 429 response + * + * // Continue with normal request handling + * } + * ``` + */ diff --git a/maternal-web/scripts/test-rate-limit.sh b/maternal-web/scripts/test-rate-limit.sh new file mode 100755 index 0000000..6a501f7 --- /dev/null +++ b/maternal-web/scripts/test-rate-limit.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Test script for rate limiting +# Tests authentication endpoint rate limit (5 requests per 15 minutes) + +echo "Testing authentication rate limiting..." +echo "Endpoint: POST /api/auth/login" +echo "Limit: 5 requests per 15 minutes" +echo "" + +BASE_URL="http://localhost:3030" + +# Make 7 requests to trigger rate limit +for i in {1..7}; do + echo "Request #$i:" + RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}\n" \ + -X POST "$BASE_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"test123"}') + + echo "$RESPONSE" + echo "---" + + # Small delay between requests + sleep 0.5 +done + +echo "" +echo "Expected: First 5 requests should go through (may fail on backend)" +echo "Expected: Requests 6-7 should return 429 Too Many Requests"