Merge pull request #2 from AeThex-Corporation/claude/find-fix-bug-mkkscfv6hiao6uvi-LBUkV

Claude/find fix bug mkkscfv6hiao6uvi lb uk v
This commit is contained in:
Anderson 2026-01-18 23:43:32 -07:00 committed by GitHub
commit fa1d0fcc70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 442 additions and 40 deletions

View file

@ -4,6 +4,11 @@ DATABASE_URL=postgresql://user:password@localhost:5432/aethex_passport
# Server Configuration # Server Configuration
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development
FRONTEND_URL=http://localhost:5173
# Development Security (ONLY for development, DO NOT enable in production)
# Allows bypassing authentication - requires BOTH NODE_ENV=development AND ALLOW_DEV_BYPASS=true
ALLOW_DEV_BYPASS=true
# Blockchain Configuration (for .aethex domain verification) # Blockchain Configuration (for .aethex domain verification)
RPC_ENDPOINT=https://polygon-mainnet.infura.io/v3/YOUR_INFURA_KEY RPC_ENDPOINT=https://polygon-mainnet.infura.io/v3/YOUR_INFURA_KEY
@ -16,7 +21,13 @@ JWT_SECRET=your-secret-key-here
RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100 RATE_LIMIT_MAX_REQUESTS=100
# TURN Server Configuration (for WebRTC NAT traversal) # TURN Server Configuration (for WebRTC NAT traversal)
# CRITICAL: These MUST be set in production - no defaults allowed
TURN_SERVER_HOST=turn.example.com TURN_SERVER_HOST=turn.example.com
TURN_SERVER_PORT=3478 TURN_SERVER_PORT=3478
TURN_SECRET=your-turn-secret-key TURN_SECRET=your-turn-secret-key
TURN_TTL=86400 TURN_TTL=86400
# Stripe Configuration (for payments)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

View file

@ -7,21 +7,28 @@ const jwt = require('jsonwebtoken');
function authenticateUser(req, res, next) { function authenticateUser(req, res, next) {
try { try {
// In development mode, allow requests without auth for testing // In development mode, allow requests without auth for testing
if (process.env.NODE_ENV === 'development') { // SECURITY: This bypass only works if BOTH conditions are met:
// 1. NODE_ENV is 'development'
// 2. ALLOW_DEV_BYPASS is explicitly set to 'true'
const isDevelopment = process.env.NODE_ENV === 'development';
const allowDevBypass = process.env.ALLOW_DEV_BYPASS === 'true';
if (isDevelopment && allowDevBypass) {
// Check for token, but if not present, use demo user // Check for token, but if not present, use demo user
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { if (!authHeader || !authHeader.startsWith('Bearer ')) {
// Use demo user for development // Use demo user for development
console.warn('⚠️ Development mode: Using demo user without authentication');
req.user = { req.user = {
id: 'demo-user-123', id: 'demo-user-123',
email: 'demo@aethex.dev' email: 'demo@aethex.dev'
}; };
return next(); return next();
} }
const token = authHeader.substring(7); const token = authHeader.substring(7);
// Try to verify, but don't fail if invalid // Try to verify, but don't fail if invalid
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_SECRET);
@ -31,15 +38,16 @@ function authenticateUser(req, res, next) {
}; };
} catch { } catch {
// Use demo user if token invalid // Use demo user if token invalid
console.warn('⚠️ Development mode: Invalid token, using demo user');
req.user = { req.user = {
id: 'demo-user-123', id: 'demo-user-123',
email: 'demo@aethex.dev' email: 'demo@aethex.dev'
}; };
} }
return next(); return next();
} }
// Production: Strict auth required // Production: Strict auth required
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;

View file

@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const db = require('../../database/db'); const db = require('../../database/db');
const notificationService = require('../../services/notificationService');
/** /**
* Stripe webhook endpoint * Stripe webhook endpoint
@ -143,8 +144,8 @@ async function handleSubscriptionDeleted(subscription) {
console.log(`[Webhook] User ${userId} downgraded to free tier`); console.log(`[Webhook] User ${userId} downgraded to free tier`);
// TODO: Send notification to user about downgrade // Send notification to user about downgrade
// Could trigger email, in-app notification, etc. await notificationService.notifySubscriptionDowngrade(userId);
} }
} }
@ -177,7 +178,13 @@ async function handlePaymentSucceeded(invoice) {
console.log(`[Webhook] Payment logged for user ${subResult.rows[0].user_id}`); console.log(`[Webhook] Payment logged for user ${subResult.rows[0].user_id}`);
// TODO: Send receipt email to user // Send receipt email to user
const amount = invoice.amount_paid / 100;
await notificationService.notifyPaymentSuccess(
subResult.rows[0].user_id,
amount,
invoice.id
);
} }
} }
} }
@ -213,8 +220,10 @@ async function handlePaymentFailed(invoice) {
console.log(`[Webhook] Failed payment logged for user ${userId}`); console.log(`[Webhook] Failed payment logged for user ${userId}`);
// TODO: Send notification to user about failed payment // Send notification to user about failed payment
// Include link to update payment method const amount = invoice.amount_due / 100;
const reason = invoice.last_payment_error?.message || 'Unknown error';
await notificationService.notifyPaymentFailed(userId, amount, reason);
} }
} }
} }
@ -232,9 +241,16 @@ async function handleTrialWillEnd(subscription) {
); );
if (subResult.rows.length > 0) { if (subResult.rows.length > 0) {
// TODO: Send notification to user that trial is ending const userId = subResult.rows[0].user_id;
// Remind them to add payment method
console.log(`[Webhook] Trial ending notification for user ${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}`);
} }
} }

View file

@ -29,6 +29,10 @@ class CallService {
[conversationId] [conversationId]
); );
if (conversationResult.rows.length === 0) {
throw new Error(`Conversation ${conversationId} not found`);
}
const conversation = conversationResult.rows[0]; const conversation = conversationResult.rows[0];
// Determine if group call // Determine if group call
@ -249,9 +253,12 @@ class CallService {
async generateTURNCredentials(userId) { async generateTURNCredentials(userId) {
const timestamp = Math.floor(Date.now() / 1000) + 86400; // 24 hour TTL const timestamp = Math.floor(Date.now() / 1000) + 86400; // 24 hour TTL
const username = `${timestamp}:${userId}`; const username = `${timestamp}:${userId}`;
// Generate credential using HMAC // Generate credential using HMAC
const turnSecret = process.env.TURN_SECRET || 'default-secret-change-me'; const turnSecret = process.env.TURN_SECRET;
if (!turnSecret) {
throw new Error('TURN_SECRET environment variable must be set for TURN server authentication');
}
const hmac = crypto.createHmac('sha1', turnSecret); const hmac = crypto.createHmac('sha1', turnSecret);
hmac.update(username); hmac.update(username);
const credential = hmac.digest('base64'); const credential = hmac.digest('base64');
@ -265,8 +272,12 @@ class CallService {
[userId, username, credential, timestamp] [userId, username, credential, timestamp]
); );
const turnHost = process.env.TURN_SERVER_HOST || 'turn.aethex.app'; const turnHost = process.env.TURN_SERVER_HOST;
const turnPort = process.env.TURN_SERVER_PORT || '3478'; const turnPort = process.env.TURN_SERVER_PORT;
if (!turnHost || !turnPort) {
throw new Error('TURN_SERVER_HOST and TURN_SERVER_PORT environment variables must be set');
}
return { return {
urls: [ urls: [

View file

@ -36,6 +36,10 @@ class GameForgeIntegrationService {
] ]
); );
if (integrationResult.rows.length === 0) {
throw new Error('Failed to create GameForge integration');
}
const integration = integrationResult.rows[0]; const integration = integrationResult.rows[0];
// Create channels // Create channels
@ -88,13 +92,17 @@ class GameForgeIntegrationService {
const description = this.getChannelDescription(channelName); const description = this.getChannelDescription(channelName);
const conversationResult = await db.query( const conversationResult = await db.query(
`INSERT INTO conversations `INSERT INTO conversations
(type, title, description, created_by, gameforge_project_id) (type, title, description, created_by, gameforge_project_id)
VALUES ('group', $1, $2, $3, $4) VALUES ('group', $1, $2, $3, $4)
RETURNING *`, RETURNING *`,
[title, description, ownerId, projectId] [title, description, ownerId, projectId]
); );
if (conversationResult.rows.length === 0) {
throw new Error('Failed to create conversation for GameForge channel');
}
const conversation = conversationResult.rows[0]; const conversation = conversationResult.rows[0];
// Add team members based on permissions // Add team members based on permissions

View file

@ -263,9 +263,37 @@ class MessagingService {
* Send a message * Send a message
*/ */
async sendMessage(userId, conversationId, { content, contentType = 'text', metadata = null, replyToId = null, identityId = null }) { async sendMessage(userId, conversationId, { content, contentType = 'text', metadata = null, replyToId = null, identityId = null }) {
// Validate required parameters
if (!userId || typeof userId !== 'string') {
throw new Error('Invalid userId parameter');
}
if (!conversationId || typeof conversationId !== 'string') {
throw new Error('Invalid conversationId parameter');
}
if (!content || typeof content !== 'string' || content.trim().length === 0) {
throw new Error('Message content cannot be empty');
}
if (content.length > 10000) {
throw new Error('Message content exceeds maximum length of 10000 characters');
}
// Validate contentType
const validContentTypes = ['text', 'image', 'file', 'audio', 'video', 'code'];
if (!validContentTypes.includes(contentType)) {
throw new Error(`Invalid contentType. Must be one of: ${validContentTypes.join(', ')}`);
}
// Validate metadata if provided
if (metadata !== null && typeof metadata !== 'object') {
throw new Error('metadata must be an object or null');
}
// Verify user is participant // Verify user is participant
await this.verifyParticipant(conversationId, userId); await this.verifyParticipant(conversationId, userId);
// In production, content should already be encrypted by client // In production, content should already be encrypted by client
// For now, we'll just store it as-is // For now, we'll just store it as-is
const contentEncrypted = content; const contentEncrypted = content;
@ -287,8 +315,13 @@ class MessagingService {
`SELECT username, verified_domain, avatar_url FROM users WHERE id = $1`, `SELECT username, verified_domain, avatar_url FROM users WHERE id = $1`,
[userId] [userId]
); );
if (userResult.rows.length === 0) {
throw new Error(`User ${userId} not found`);
}
const user = userResult.rows[0]; const user = userResult.rows[0];
return { return {
id: message.id, id: message.id,
conversationId: message.conversation_id, conversationId: message.conversation_id,

View file

@ -0,0 +1,223 @@
/**
* Notification Service
* Handles sending notifications to users via multiple channels
*/
const db = require('../database/db');
class NotificationService {
/**
* Send notification to user
* @param {string} userId - User ID to notify
* @param {Object} notification - Notification details
* @param {string} notification.type - Type of notification (email, in-app, both)
* @param {string} notification.category - Category (payment, subscription, system, etc.)
* @param {string} notification.title - Notification title
* @param {string} notification.message - Notification message
* @param {Object} notification.metadata - Additional metadata
*/
async sendNotification(userId, notification) {
const { type = 'in-app', category, title, message, metadata = {} } = notification;
try {
// Store notification in database
const result = await db.query(
`INSERT INTO notifications
(user_id, type, category, title, message, metadata, read, created_at)
VALUES ($1, $2, $3, $4, $5, $6, false, NOW())
RETURNING *`,
[userId, type, category, title, message, JSON.stringify(metadata)]
);
const savedNotification = result.rows[0];
// If email notification requested, queue email
if (type === 'email' || type === 'both') {
await this.queueEmail(userId, notification);
}
// Emit real-time notification via Socket.io if available
if (global.io) {
global.io.to(`user:${userId}`).emit('notification', {
id: savedNotification.id,
category,
title,
message,
metadata,
createdAt: savedNotification.created_at
});
}
return savedNotification;
} catch (error) {
console.error('Failed to send notification:', error);
// Don't throw - notifications should be best-effort
// Log error but don't fail the parent operation
return null;
}
}
/**
* Queue email for sending
* In production, this would integrate with an email service like SendGrid, SES, etc.
*/
async queueEmail(userId, notification) {
const { title, message, metadata = {} } = notification;
try {
// Get user email
const userResult = await db.query(
`SELECT email, username FROM users WHERE id = $1`,
[userId]
);
if (userResult.rows.length === 0) {
console.error(`User ${userId} not found for email notification`);
return;
}
const user = userResult.rows[0];
// Queue email in database (to be processed by email worker)
await db.query(
`INSERT INTO email_queue
(user_id, recipient_email, subject, body, template, metadata, status, created_at)
VALUES ($1, $2, $3, $4, $5, $6, 'pending', NOW())`,
[
userId,
user.email,
title,
message,
metadata.template || 'default',
JSON.stringify({ ...metadata, username: user.username })
]
);
console.log(`Email queued for ${user.email}: ${title}`);
} catch (error) {
console.error('Failed to queue email:', error);
}
}
/**
* Send subscription downgrade notification
*/
async notifySubscriptionDowngrade(userId) {
return this.sendNotification(userId, {
type: 'both',
category: 'subscription',
title: 'Subscription Cancelled',
message: 'Your premium subscription has been cancelled and your account has been downgraded to the free tier. You still have access to premium features until the end of your billing period.',
metadata: {
template: 'subscription-downgrade',
action: {
label: 'Resubscribe',
url: '/premium'
}
}
});
}
/**
* Send payment receipt
*/
async notifyPaymentSuccess(userId, amount, invoiceId) {
return this.sendNotification(userId, {
type: 'email',
category: 'payment',
title: 'Payment Receipt',
message: `Your payment of $${amount.toFixed(2)} has been processed successfully. Thank you for your continued subscription!`,
metadata: {
template: 'payment-receipt',
amount,
invoiceId,
action: {
label: 'View Invoice',
url: `/billing/invoices/${invoiceId}`
}
}
});
}
/**
* Send payment failure notification
*/
async notifyPaymentFailed(userId, amount, reason = 'Unknown') {
return this.sendNotification(userId, {
type: 'both',
category: 'payment',
title: 'Payment Failed',
message: `We were unable to process your payment of $${amount.toFixed(2)}. Please update your payment method to continue your subscription.`,
metadata: {
template: 'payment-failed',
amount,
reason,
action: {
label: 'Update Payment Method',
url: '/settings/billing'
}
}
});
}
/**
* Send trial ending notification
*/
async notifyTrialEnding(userId, daysRemaining = 3) {
return this.sendNotification(userId, {
type: 'both',
category: 'subscription',
title: 'Trial Ending Soon',
message: `Your premium trial will end in ${daysRemaining} days. Add a payment method to continue enjoying premium features without interruption.`,
metadata: {
template: 'trial-ending',
daysRemaining,
action: {
label: 'Add Payment Method',
url: '/settings/billing'
}
}
});
}
/**
* Get unread notifications for user
*/
async getUnreadNotifications(userId, limit = 50) {
const result = await db.query(
`SELECT * FROM notifications
WHERE user_id = $1 AND read = false
ORDER BY created_at DESC
LIMIT $2`,
[userId, limit]
);
return result.rows;
}
/**
* Mark notification as read
*/
async markAsRead(notificationId, userId) {
await db.query(
`UPDATE notifications
SET read = true, read_at = NOW()
WHERE id = $1 AND user_id = $2`,
[notificationId, userId]
);
}
/**
* Mark all notifications as read for user
*/
async markAllAsRead(userId) {
await db.query(
`UPDATE notifications
SET read = true, read_at = NOW()
WHERE user_id = $1 AND read = false`,
[userId]
);
}
}
module.exports = new NotificationService();

View file

@ -86,21 +86,88 @@ class SocketService {
// Join all conversations user is part of // Join all conversations user is part of
this.joinUserConversations(socket, userId); this.joinUserConversations(socket, userId);
// Handle events // Handle events with error handling
socket.on('join_conversation', (data) => this.handleJoinConversation(socket, data)); socket.on('join_conversation', async (data) => {
socket.on('leave_conversation', (data) => this.handleLeaveConversation(socket, data)); try {
socket.on('typing_start', (data) => this.handleTypingStart(socket, data)); await this.handleJoinConversation(socket, data);
socket.on('typing_stop', (data) => this.handleTypingStop(socket, data)); } catch (error) {
socket.on('call_signal', (data) => this.handleCallSignal(socket, data)); console.error('Error handling join_conversation:', error);
socket.emit('error', { event: 'join_conversation', message: error.message });
}
});
socket.on('leave_conversation', async (data) => {
try {
await this.handleLeaveConversation(socket, data);
} catch (error) {
console.error('Error handling leave_conversation:', error);
socket.emit('error', { event: 'leave_conversation', message: error.message });
}
});
socket.on('typing_start', async (data) => {
try {
await this.handleTypingStart(socket, data);
} catch (error) {
console.error('Error handling typing_start:', error);
socket.emit('error', { event: 'typing_start', message: error.message });
}
});
socket.on('typing_stop', async (data) => {
try {
await this.handleTypingStop(socket, data);
} catch (error) {
console.error('Error handling typing_stop:', error);
socket.emit('error', { event: 'typing_stop', message: error.message });
}
});
socket.on('call_signal', async (data) => {
try {
await this.handleCallSignal(socket, data);
} catch (error) {
console.error('Error handling call_signal:', error);
socket.emit('error', { event: 'call_signal', message: error.message });
}
});
// Call signaling events // Call signaling events
socket.on('call:offer', (data) => this.handleCallOffer(socket, data)); socket.on('call:offer', async (data) => {
socket.on('call:answer', (data) => this.handleCallAnswer(socket, data)); try {
socket.on('call:ice-candidate', (data) => this.handleIceCandidate(socket, data)); await this.handleCallOffer(socket, data);
} catch (error) {
console.error('Error handling call:offer:', error);
socket.emit('call:error', { type: 'offer', message: error.message });
}
});
socket.on('call:answer', async (data) => {
try {
await this.handleCallAnswer(socket, data);
} catch (error) {
console.error('Error handling call:answer:', error);
socket.emit('call:error', { type: 'answer', message: error.message });
}
});
socket.on('call:ice-candidate', async (data) => {
try {
await this.handleIceCandidate(socket, data);
} catch (error) {
console.error('Error handling call:ice-candidate:', error);
socket.emit('call:error', { type: 'ice-candidate', message: error.message });
}
});
// Disconnect handler // Disconnect handler
socket.on('disconnect', () => { socket.on('disconnect', async () => {
this.handleDisconnect(socket, userId); try {
await this.handleDisconnect(socket, userId);
} catch (error) {
console.error('Error handling disconnect:', error);
// Don't emit error as socket is disconnecting
}
}); });
} }

View file

@ -17,8 +17,9 @@ export default function Chat() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [typingUsers, setTypingUsers] = useState(new Set()); const [typingUsers, setTypingUsers] = useState(new Set());
const [error, setError] = useState(null); const [error, setError] = useState(null);
const typingTimeoutRef = useRef(null); const typingTimeoutRef = useRef(null);
const activeConversationRef = useRef(null);
// Load conversations on mount // Load conversations on mount
useEffect(() => { useEffect(() => {
@ -101,7 +102,9 @@ export default function Chat() {
const data = await response.json(); const data = await response.json();
if (data.success) { // Only update messages if this conversation is still active
// This prevents race conditions when rapidly switching conversations
if (data.success && activeConversationRef.current === conversationId) {
setMessages(data.messages); setMessages(data.messages);
// Mark as read // Mark as read
@ -116,8 +119,16 @@ export default function Chat() {
// Select conversation // Select conversation
const selectConversation = async (conversation) => { const selectConversation = async (conversation) => {
const conversationId = conversation.id;
// Update state and ref immediately to prevent race conditions
setActiveConversation(conversation); setActiveConversation(conversation);
await loadMessages(conversation.id); activeConversationRef.current = conversationId;
// Clear current messages while loading
setMessages([]);
await loadMessages(conversationId);
}; };
// Handle new message // Handle new message

View file

@ -33,7 +33,8 @@ export function AuthProvider({ children }) {
const login = async (email, password) => { const login = async (email, password) => {
// Mock login - in production, call actual API // Mock login - in production, call actual API
try { try {
const response = await fetch('http://localhost:3000/api/auth/login', { const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000';
const response = await fetch(`${apiUrl}/api/auth/login`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }) body: JSON.stringify({ email, password })

View file

@ -175,6 +175,7 @@ export async function encryptMessage(message, recipientPublicKeys) {
// Encrypt AES key for each recipient with their RSA public key // Encrypt AES key for each recipient with their RSA public key
const encryptedKeys = {}; const encryptedKeys = {};
const failedRecipients = [];
for (const recipientKeyB64 of recipientPublicKeys) { for (const recipientKeyB64 of recipientPublicKeys) {
try { try {
@ -201,10 +202,22 @@ export async function encryptMessage(message, recipientPublicKeys) {
encryptedKeys[recipientKeyB64] = arrayBufferToBase64(encryptedKey); encryptedKeys[recipientKeyB64] = arrayBufferToBase64(encryptedKey);
} catch (error) { } catch (error) {
console.error('Failed to encrypt for recipient:', error); failedRecipients.push({
publicKey: recipientKeyB64.substring(0, 20) + '...', // Truncate for error message
error: error.message
});
} }
} }
// Throw error if any encryptions failed to prevent partial message delivery
if (failedRecipients.length > 0) {
throw new Error(
`Failed to encrypt message for ${failedRecipients.length} recipient(s): ${
failedRecipients.map(r => r.error).join(', ')
}`
);
}
return { return {
ciphertext: arrayBufferToBase64(encryptedMessage), ciphertext: arrayBufferToBase64(encryptedMessage),
iv: arrayBufferToBase64(iv), iv: arrayBufferToBase64(iv),