AeThex-Connect/src/backend/routes/webhooks/stripeWebhook.js
Claude 1dcb357313
Implement comprehensive fixes for remaining critical issues
This commit addresses the remaining high-priority issues identified in the
comprehensive codebase analysis, implementing proper notification systems,
error handling, input validation, and race condition fixes.

1. CRITICAL: Implement Stripe webhook notifications
   - Created new NotificationService for centralized notification handling
   - Supports both in-app notifications and email queuing
   - Implemented all 4 missing webhook notifications:
     * Subscription downgrade notifications
     * Payment success receipts
     * Payment failure alerts with recovery action
     * Trial ending reminders with days remaining calculation
   - Notifications stored in database and emitted via Socket.io
   - File: src/backend/services/notificationService.js (NEW)
   - Updated: src/backend/routes/webhooks/stripeWebhook.js

2. HIGH: Add comprehensive error handling to socket event handlers
   - Wrapped all socket event handlers in try-catch blocks
   - Emit error events back to clients when operations fail
   - Prevents server crashes from unhandled socket errors
   - Provides user feedback for failed socket operations
   - File: src/backend/services/socketService.js

3. HIGH: Fix race condition in Chat component
   - Added activeConversationRef to track current conversation
   - Check conversation ID before updating messages after async load
   - Clear messages immediately when switching conversations
   - Prevents stale messages from appearing when rapidly switching
   - File: src/frontend/components/Chat/Chat.jsx

4. HIGH: Add input validation to messaging service
   - Validate userId and conversationId are valid strings
   - Ensure message content is not empty and under 10K chars
   - Validate contentType against allowed types
   - Validate metadata structure
   - Provides clear error messages for invalid input
   - File: src/backend/services/messagingService.js

5. MEDIUM: Replace hardcoded URLs with environment variables
   - Updated AuthContext to use VITE_API_URL env variable
   - Maintains localhost fallback for development
   - File: src/frontend/contexts/AuthContext.jsx

6. Documentation: Update .env.example
   - Added FRONTEND_URL configuration
   - Documented ALLOW_DEV_BYPASS security flag
   - Added critical warnings for TURN server configuration
   - Added Stripe configuration variables
   - File: .env.example

These fixes significantly improve:
- User experience (notifications for all payment events)
- System reliability (proper error handling, race condition fixes)
- Security (input validation prevents malicious input)
- Maintainability (proper environment configuration)
2026-01-19 06:41:28 +00:00

257 lines
7.2 KiB
JavaScript

const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const db = require('../../database/db');
const notificationService = require('../../services/notificationService');
/**
* Stripe webhook endpoint
* Handles subscription lifecycle events
*
* Note: This endpoint uses express.raw() middleware for signature verification
* Set this up in server.js before the main JSON body parser
*/
router.post('/', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('⚠️ Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
console.log(`✅ Webhook received: ${event.type}`);
// Handle event
try {
switch (event.type) {
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event.data.object);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object);
break;
case 'customer.subscription.trial_will_end':
await handleTrialWillEnd(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
/**
* Handle subscription created
*/
async function handleSubscriptionCreated(subscription) {
console.log(`[Webhook] Subscription created: ${subscription.id}`);
// Subscription is already created in premiumService.subscribe()
// This webhook just confirms it
}
/**
* Handle subscription updated
*/
async function handleSubscriptionUpdated(subscription) {
console.log(`[Webhook] Subscription updated: ${subscription.id}`);
await db.query(
`UPDATE premium_subscriptions
SET current_period_end = to_timestamp($2),
cancel_at_period_end = $3,
status = $4,
updated_at = NOW()
WHERE stripe_subscription_id = $1`,
[
subscription.id,
subscription.current_period_end,
subscription.cancel_at_period_end,
subscription.status
]
);
// Update domain expiration if applicable
await db.query(
`UPDATE blockchain_domains
SET expires_at = to_timestamp($2)
WHERE owner_user_id IN (
SELECT user_id FROM premium_subscriptions
WHERE stripe_subscription_id = $1
)`,
[subscription.id, subscription.current_period_end]
);
}
/**
* Handle subscription deleted/cancelled
*/
async function handleSubscriptionDeleted(subscription) {
console.log(`[Webhook] Subscription deleted: ${subscription.id}`);
// Update subscription status
await db.query(
`UPDATE premium_subscriptions
SET status = 'cancelled',
cancelled_at = NOW(),
updated_at = NOW()
WHERE stripe_subscription_id = $1`,
[subscription.id]
);
// Downgrade user to free tier
const subResult = await db.query(
`SELECT user_id FROM premium_subscriptions
WHERE stripe_subscription_id = $1`,
[subscription.id]
);
if (subResult.rows.length > 0) {
const userId = subResult.rows[0].user_id;
await db.query(
`UPDATE users SET premium_tier = 'free' WHERE id = $1`,
[userId]
);
console.log(`[Webhook] User ${userId} downgraded to free tier`);
// Send notification to user about downgrade
await notificationService.notifySubscriptionDowngrade(userId);
}
}
/**
* Handle successful payment
*/
async function handlePaymentSucceeded(invoice) {
console.log(`[Webhook] Payment succeeded for invoice: ${invoice.id}`);
// Log the transaction
if (invoice.customer && invoice.subscription) {
const subResult = await db.query(
`SELECT user_id FROM premium_subscriptions
WHERE stripe_subscription_id = $1`,
[invoice.subscription]
);
if (subResult.rows.length > 0) {
await db.query(
`INSERT INTO payment_transactions
(user_id, transaction_type, amount_usd, stripe_payment_intent_id, stripe_invoice_id, status)
VALUES ($1, 'subscription_renewal', $2, $3, $4, 'succeeded')`,
[
subResult.rows[0].user_id,
invoice.amount_paid / 100, // Convert from cents
invoice.payment_intent,
invoice.id
]
);
console.log(`[Webhook] Payment logged for user ${subResult.rows[0].user_id}`);
// Send receipt email to user
const amount = invoice.amount_paid / 100;
await notificationService.notifyPaymentSuccess(
subResult.rows[0].user_id,
amount,
invoice.id
);
}
}
}
/**
* Handle failed payment
*/
async function handlePaymentFailed(invoice) {
console.log(`[Webhook] Payment failed for invoice: ${invoice.id}`);
// Log failed transaction
if (invoice.customer && invoice.subscription) {
const subResult = await db.query(
`SELECT user_id FROM premium_subscriptions
WHERE stripe_subscription_id = $1`,
[invoice.subscription]
);
if (subResult.rows.length > 0) {
const userId = subResult.rows[0].user_id;
await db.query(
`INSERT INTO payment_transactions
(user_id, transaction_type, amount_usd, stripe_payment_intent_id, stripe_invoice_id, status)
VALUES ($1, 'subscription_renewal', $2, $3, $4, 'failed')`,
[
userId,
invoice.amount_due / 100,
invoice.payment_intent,
invoice.id
]
);
console.log(`[Webhook] Failed payment logged for user ${userId}`);
// Send notification to user about failed payment
const amount = invoice.amount_due / 100;
const reason = invoice.last_payment_error?.message || 'Unknown error';
await notificationService.notifyPaymentFailed(userId, amount, reason);
}
}
}
/**
* Handle trial ending soon
*/
async function handleTrialWillEnd(subscription) {
console.log(`[Webhook] Trial ending soon for subscription: ${subscription.id}`);
const subResult = await db.query(
`SELECT user_id FROM premium_subscriptions
WHERE stripe_subscription_id = $1`,
[subscription.id]
);
if (subResult.rows.length > 0) {
const userId = subResult.rows[0].user_id;
// Calculate days remaining in trial
const trialEnd = new Date(subscription.trial_end * 1000);
const now = new Date();
const daysRemaining = Math.ceil((trialEnd - now) / (1000 * 60 * 60 * 24));
// Send notification to user that trial is ending
await notificationService.notifyTrialEnding(userId, daysRemaining);
console.log(`[Webhook] Trial ending notification sent to user ${userId}`);
}
}
module.exports = router;