Overview
Connect World supports two payment methods: Stripe for card payments and PayPal for PayPal account payments. Both integrations include comprehensive security measures, rate limiting, and price validation.
Stripe Credit and debit card processing with automatic payment methods
PayPal PayPal account payments with order capture flow
Payment Methods
Payment methods are defined as a union type:
export type PaymentMethod = "stripe" | "paypal" ;
Source: src/domain/entities/Order.ts:1
Stripe Integration
Payment Intent Creation
The Stripe API endpoint creates payment intents with comprehensive validation:
export async function POST ( req : NextRequest ) {
// Rate limit: 10 payment-intent attempts per IP per 15 minutes
const ip = getClientIp ( req );
if ( ! checkRateLimit ( `stripe: ${ ip } ` , 10 , 15 * 60 * 1000 )) {
return NextResponse . json (
{ error: "Demasiadas solicitudes. Intenta de nuevo en unos minutos." },
{ status: 429 }
);
}
try {
const body = await req . json ();
const planId = sanitizeString ( body . planId , 50 );
const months = sanitizeNumber ( body . months , 1 , 12 );
const amount = sanitizeNumber ( body . amount , 1 , 10000 );
// Validation logic...
const paymentIntent = await stripe . paymentIntents . create ({
amount: Math . round ( amount * 100 ),
currency: "usd" ,
automatic_payment_methods: { enabled: true },
metadata: { planId , months: String ( months ) },
});
return NextResponse . json ({ clientSecret: paymentIntent . client_secret });
} catch ( error : unknown ) {
// Error handling...
}
}
Source: src/app/api/stripe/route.ts:11-60
Stripe Configuration
import Stripe from "stripe" ;
const stripe = new Stripe ( process . env . STRIPE_SECRET_KEY ! );
Source: src/app/api/stripe/route.ts:7
The Stripe secret key must be configured in your environment variables as STRIPE_SECRET_KEY.
Valid Payment Durations
const VALID_MONTHS = [ 1 , 2 , 3 , 6 , 12 ] as const ;
Source: src/app/api/stripe/route.ts:9
PayPal Integration
Order Creation Flow
PayPal uses a two-step process: create order, then capture payment.
Create Order
Capture Order
export async function POST ( req : NextRequest ) {
// Rate limit: 10 PayPal order creations per IP per 15 minutes
const ip = getClientIp ( req );
if ( ! checkRateLimit ( `paypal-create: ${ ip } ` , 10 , 15 * 60 * 1000 )) {
return NextResponse . json (
{ error: "Demasiadas solicitudes. Intenta de nuevo en unos minutos." },
{ status: 429 }
);
}
// Validation and sanitization...
const accessToken = await getPayPalAccessToken ();
const orderPayload = {
intent: "CAPTURE" ,
purchase_units: [
{
amount: { currency_code: "USD" , value: String ( Number ( amount ). toFixed ( 2 )) },
description: `Connect World Plan ${ planId } ${ months } mo` ,
},
],
};
const { data } = await axios . post (
` ${ PAYPAL_BASE } /v2/checkout/orders` ,
orderPayload ,
{
headers: {
Authorization: `Bearer ${ accessToken } ` ,
"Content-Type" : "application/json" ,
},
}
);
return NextResponse . json ({ id: data . id });
}
Source: src/app/api/paypal/create-order/route.ts:29-101 export async function POST ( req : NextRequest ) {
try {
const { orderId } = await req . json ();
const accessToken = await getPayPalAccessToken ();
const { data } = await axios . post (
` ${ PAYPAL_BASE } /v2/checkout/orders/ ${ orderId } /capture` ,
{},
{
headers: {
Authorization: `Bearer ${ accessToken } ` ,
"Content-Type" : "application/json" ,
},
}
);
return NextResponse . json ({ id: data . id , status: data . status });
} catch ( error : unknown ) {
// Error handling...
}
}
Source: src/app/api/paypal/capture-order/route.ts:25-54
PayPal Authentication
PayPal requires OAuth2 authentication for each API request:
async function getPayPalAccessToken () : Promise < string > {
const credentials = Buffer . from (
` ${ process . env . NEXT_PUBLIC_PAYPAL_CLIENT_ID } : ${ process . env . PAYPAL_CLIENT_SECRET } `
). toString ( "base64" );
const { data } = await axios . post (
` ${ PAYPAL_BASE } /v1/oauth2/token` ,
"grant_type=client_credentials" ,
{
headers: {
Authorization: `Basic ${ credentials } ` ,
"Content-Type" : "application/x-www-form-urlencoded" ,
},
}
);
return data . access_token ;
}
Source: src/app/api/paypal/create-order/route.ts:10-27
PayPal Configuration
const PAYPAL_BASE = process . env . PAYPAL_BASE_URL ?? "https://api-m.sandbox.paypal.com" ;
Source: src/app/api/paypal/create-order/route.ts:7
Configure PAYPAL_BASE_URL to point to production (https://api-m.paypal.com) when going live.
Security Features
All payment inputs are sanitized to prevent injection attacks:
import { sanitizeNumber , sanitizeString } from "@/lib/sanitize" ;
const planId = sanitizeString ( body . planId , 50 );
const months = sanitizeNumber ( body . months , 1 , 12 );
const amount = sanitizeNumber ( body . amount , 1 , 10000 );
Source: src/app/api/stripe/route.ts:4, 24-26
Sanitization Functions
Input Sanitization Implementation
Rate Limiting
Both payment endpoints implement rate limiting to prevent abuse:
export function checkRateLimit ( key : string , maxReqs : number , windowMs : number ) : boolean {
const now = Date . now ();
const entry = store . get ( key );
if ( ! entry || now > entry . resetAt ) {
store . set ( key , { count: 1 , resetAt: now + windowMs });
return true ;
}
if ( entry . count >= maxReqs ) return false ;
entry . count += 1 ;
return true ;
}
Source: src/lib/rateLimiter.ts:30-43
Stripe Rate Limit 10 payment intents per IP per 15 minutes
PayPal Rate Limit 10 order creations per IP per 15 minutes
Price Validation
Critical security feature: server-side price verification prevents price tampering:
// Validate planId exists and months is a valid duration
const plan = PLANS . find (( p ) => p . id === planId );
if ( ! plan ) {
return NextResponse . json ({ error: "Plan inválido." }, { status: 400 });
}
if ( ! ( VALID_MONTHS as readonly number []). includes ( months )) {
return NextResponse . json ({ error: "Duración inválida." }, { status: 400 });
}
// Validate amount matches the real price (prevents price tampering)
const expectedPrice = plan . prices . find (( p ) => p . months === months )?. price ;
if ( expectedPrice === undefined || Math . abs ( amount - expectedPrice ) > 0.01 ) {
return NextResponse . json ({ error: "Monto inválido." }, { status: 400 });
}
Source: src/app/api/stripe/route.ts:33-45
Never trust client-provided prices. Always validate against server-side plan definitions before processing payments.
Error Handling
Both payment integrations include comprehensive error handling:
Stripe Errors
PayPal Errors
try {
// Payment processing...
} catch ( error : unknown ) {
const message = error instanceof Error ? error . message : "Internal server error" ;
console . error ( "[POST /api/stripe]" , message );
return NextResponse . json ({ error: message }, { status: 500 });
}
Source: src/app/api/stripe/route.ts:55-59 catch ( error : unknown ) {
const axiosError = error as { response ?: { data ?: unknown ; status ?: number } };
const paypalDetail = axiosError ?. response ?. data ;
console . error ( "[PayPal create-order]" , paypalDetail ?? error );
const message = paypalDetail
? JSON . stringify ( paypalDetail )
: error instanceof Error
? error . message
: "Failed to create PayPal order" ;
return NextResponse . json ({ error: message }, { status: axiosError ?. response ?. status ?? 500 });
}
Source: src/app/api/paypal/create-order/route.ts:90-100
Payment Flow Diagram
Environment Variables
STRIPE_SECRET_KEY = sk_test_...
Use sandbox URLs for testing and production URLs for live environments.
Best Practices
Always validate prices server-side - Never trust client-provided amounts
Implement rate limiting - Prevent abuse and brute force attacks
Sanitize all inputs - Protect against injection attacks
Use metadata - Store plan and duration information in payment metadata
Log errors comprehensively - Include PayPal response details for debugging
Handle edge cases - Validate plans exist and durations are allowed
Use environment variables - Never hardcode API keys or secrets