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)
257 lines
7.2 KiB
JavaScript
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;
|