feat: implement complete Phase 2 frontend foundation with React 18
Major milestone: Frontend implementation complete for Shattered Void MMO FRONTEND IMPLEMENTATION: - React 18 + TypeScript + Vite development environment - Tailwind CSS with custom dark theme for sci-fi aesthetic - Zustand state management with authentication persistence - Socket.io WebSocket client with auto-reconnection - Protected routing with authentication guards - Responsive design with mobile-first approach AUTHENTICATION SYSTEM: - Login/register forms with comprehensive validation - JWT token management with localStorage persistence - Password strength validation and user feedback - Protected routes and authentication guards CORE GAME INTERFACE: - Colony management dashboard with real-time updates - Resource display with live production tracking - WebSocket integration for real-time game events - Navigation with connection status indicator - Toast notifications for user feedback BACKEND ENHANCEMENTS: - Complete Research System with technology tree (23 technologies) - Fleet Management System with ship designs and movement - Enhanced Authentication with email verification and password reset - Complete game tick integration for all systems - Advanced WebSocket events for real-time updates ARCHITECTURE FEATURES: - Type-safe TypeScript throughout - Component-based architecture with reusable UI elements - API client with request/response interceptors - Error handling and loading states - Performance optimized builds with code splitting Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met) Ready for: Colony management, fleet operations, research interface Next: Enhanced gameplay features and admin interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8d9ef427be
commit
d41d1e8125
130 changed files with 33588 additions and 14817 deletions
|
|
@ -9,65 +9,65 @@ const logger = require('../utils/logger');
|
|||
|
||||
// Rate limiting configuration
|
||||
const RATE_LIMIT_CONFIG = {
|
||||
// Global API rate limits
|
||||
global: {
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // 1000 requests per window
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false
|
||||
},
|
||||
// Global API rate limits
|
||||
global: {
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // 1000 requests per window
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
},
|
||||
|
||||
// Authentication endpoints (more restrictive)
|
||||
auth: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10, // 10 attempts per window
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true, // Don't count successful logins
|
||||
skipFailedRequests: false
|
||||
},
|
||||
// Authentication endpoints (more restrictive)
|
||||
auth: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10, // 10 attempts per window
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true, // Don't count successful logins
|
||||
skipFailedRequests: false,
|
||||
},
|
||||
|
||||
// Player API endpoints
|
||||
player: {
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 120, // 120 requests per minute
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false
|
||||
},
|
||||
// Player API endpoints
|
||||
player: {
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 120, // 120 requests per minute
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
},
|
||||
|
||||
// Admin API endpoints (more lenient for legitimate admin users)
|
||||
admin: {
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 300, // 300 requests per minute
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false
|
||||
},
|
||||
// Admin API endpoints (more lenient for legitimate admin users)
|
||||
admin: {
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 300, // 300 requests per minute
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
},
|
||||
|
||||
// Game action endpoints (prevent spam)
|
||||
gameAction: {
|
||||
windowMs: 30 * 1000, // 30 seconds
|
||||
max: 30, // 30 actions per 30 seconds
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: true
|
||||
},
|
||||
// Game action endpoints (prevent spam)
|
||||
gameAction: {
|
||||
windowMs: 30 * 1000, // 30 seconds
|
||||
max: 30, // 30 actions per 30 seconds
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: true,
|
||||
},
|
||||
|
||||
// Message sending (prevent spam)
|
||||
messaging: {
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 10, // 10 messages per 5 minutes
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: true
|
||||
}
|
||||
// Message sending (prevent spam)
|
||||
messaging: {
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 10, // 10 messages per 5 minutes
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -75,34 +75,34 @@ const RATE_LIMIT_CONFIG = {
|
|||
* @returns {Object|null} Redis store or null if Redis unavailable
|
||||
*/
|
||||
function createRedisStore() {
|
||||
try {
|
||||
const redis = getRedisClient();
|
||||
if (!redis) {
|
||||
logger.warn('Redis not available for rate limiting, using memory store');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create Redis store for express-rate-limit
|
||||
try {
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
|
||||
return new RedisStore({
|
||||
sendCommand: (...args) => redis.sendCommand(args),
|
||||
prefix: 'rl:' // Rate limit prefix
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create RedisStore, falling back to memory store', {
|
||||
error: error.message
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create Redis store for rate limiting', {
|
||||
error: error.message
|
||||
});
|
||||
return null;
|
||||
try {
|
||||
const redis = getRedisClient();
|
||||
if (!redis) {
|
||||
logger.warn('Redis not available for rate limiting, using memory store');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create Redis store for express-rate-limit
|
||||
try {
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
|
||||
return new RedisStore({
|
||||
sendCommand: (...args) => redis.sendCommand(args),
|
||||
prefix: 'rl:', // Rate limit prefix
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create RedisStore, falling back to memory store', {
|
||||
error: error.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create Redis store for rate limiting', {
|
||||
error: error.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,11 +111,11 @@ function createRedisStore() {
|
|||
* @returns {Function} Key generator function
|
||||
*/
|
||||
function createKeyGenerator(prefix = 'global') {
|
||||
return (req) => {
|
||||
const ip = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const userId = req.user?.playerId || req.user?.adminId || 'anonymous';
|
||||
return `${prefix}:${userId}:${ip}`;
|
||||
};
|
||||
return (req) => {
|
||||
const ip = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const userId = req.user?.playerId || req.user?.adminId || 'anonymous';
|
||||
return `${prefix}:${userId}:${ip}`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,32 +124,32 @@ function createKeyGenerator(prefix = 'global') {
|
|||
* @returns {Function} Rate limit handler function
|
||||
*/
|
||||
function createRateLimitHandler(type) {
|
||||
return (req, res) => {
|
||||
const correlationId = req.correlationId;
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const userId = req.user?.playerId || req.user?.adminId;
|
||||
const userType = req.user?.type || 'anonymous';
|
||||
return (req, res) => {
|
||||
const correlationId = req.correlationId;
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const userId = req.user?.playerId || req.user?.adminId;
|
||||
const userType = req.user?.type || 'anonymous';
|
||||
|
||||
logger.warn('Rate limit exceeded', {
|
||||
correlationId,
|
||||
type,
|
||||
ip,
|
||||
userId,
|
||||
userType,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
userAgent: req.get('User-Agent'),
|
||||
retryAfter: res.get('Retry-After')
|
||||
});
|
||||
logger.warn('Rate limit exceeded', {
|
||||
correlationId,
|
||||
type,
|
||||
ip,
|
||||
userId,
|
||||
userType,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
userAgent: req.get('User-Agent'),
|
||||
retryAfter: res.get('Retry-After'),
|
||||
});
|
||||
|
||||
return res.status(429).json({
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
type: type,
|
||||
retryAfter: res.get('Retry-After'),
|
||||
correlationId
|
||||
});
|
||||
};
|
||||
return res.status(429).json({
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
type,
|
||||
retryAfter: res.get('Retry-After'),
|
||||
correlationId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -159,31 +159,31 @@ function createRateLimitHandler(type) {
|
|||
* @returns {Function} Skip function
|
||||
*/
|
||||
function createSkipFunction(skipPaths = [], skipIPs = []) {
|
||||
return (req) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
|
||||
// Skip health checks
|
||||
if (req.path === '/health' || req.path === '/api/health') {
|
||||
return true;
|
||||
}
|
||||
return (req) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
|
||||
// Skip specified paths
|
||||
if (skipPaths.some(path => req.path.startsWith(path))) {
|
||||
return true;
|
||||
}
|
||||
// Skip health checks
|
||||
if (req.path === '/health' || req.path === '/api/health') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip specified IPs (for development/testing)
|
||||
if (skipIPs.includes(ip)) {
|
||||
return true;
|
||||
}
|
||||
// Skip specified paths
|
||||
if (skipPaths.some(path => req.path.startsWith(path))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip if rate limiting is disabled
|
||||
if (process.env.DISABLE_RATE_LIMITING === 'true') {
|
||||
return true;
|
||||
}
|
||||
// Skip specified IPs (for development/testing)
|
||||
if (skipIPs.includes(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
// Skip if rate limiting is disabled
|
||||
if (process.env.DISABLE_RATE_LIMITING === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -193,40 +193,40 @@ function createSkipFunction(skipPaths = [], skipIPs = []) {
|
|||
* @returns {Function} Rate limiter middleware
|
||||
*/
|
||||
function createRateLimiter(type, customConfig = {}) {
|
||||
const config = { ...RATE_LIMIT_CONFIG[type], ...customConfig };
|
||||
const store = createRedisStore();
|
||||
const config = { ...RATE_LIMIT_CONFIG[type], ...customConfig };
|
||||
const store = createRedisStore();
|
||||
|
||||
const rateLimiter = rateLimit({
|
||||
...config,
|
||||
store,
|
||||
keyGenerator: createKeyGenerator(type),
|
||||
handler: createRateLimitHandler(type),
|
||||
skip: createSkipFunction(),
|
||||
// Note: onLimitReached is deprecated in express-rate-limit v7
|
||||
// Removed for compatibility
|
||||
});
|
||||
const rateLimiter = rateLimit({
|
||||
...config,
|
||||
store,
|
||||
keyGenerator: createKeyGenerator(type),
|
||||
handler: createRateLimitHandler(type),
|
||||
skip: createSkipFunction(),
|
||||
// Note: onLimitReached is deprecated in express-rate-limit v7
|
||||
// Removed for compatibility
|
||||
});
|
||||
|
||||
// Log rate limiter creation
|
||||
logger.info('Rate limiter created', {
|
||||
type,
|
||||
windowMs: config.windowMs,
|
||||
max: config.max,
|
||||
useRedis: !!store
|
||||
});
|
||||
// Log rate limiter creation
|
||||
logger.info('Rate limiter created', {
|
||||
type,
|
||||
windowMs: config.windowMs,
|
||||
max: config.max,
|
||||
useRedis: !!store,
|
||||
});
|
||||
|
||||
return rateLimiter;
|
||||
return rateLimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured rate limiters
|
||||
*/
|
||||
const rateLimiters = {
|
||||
global: createRateLimiter('global'),
|
||||
auth: createRateLimiter('auth'),
|
||||
player: createRateLimiter('player'),
|
||||
admin: createRateLimiter('admin'),
|
||||
gameAction: createRateLimiter('gameAction'),
|
||||
messaging: createRateLimiter('messaging')
|
||||
global: createRateLimiter('global'),
|
||||
auth: createRateLimiter('auth'),
|
||||
player: createRateLimiter('player'),
|
||||
admin: createRateLimiter('admin'),
|
||||
gameAction: createRateLimiter('gameAction'),
|
||||
messaging: createRateLimiter('messaging'),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -236,12 +236,12 @@ const rateLimiters = {
|
|||
* @param {Function} next - Express next function
|
||||
*/
|
||||
function addRateLimitHeaders(req, res, next) {
|
||||
// Add custom headers for client information
|
||||
res.set({
|
||||
'X-RateLimit-Policy': 'See API documentation for rate limiting details'
|
||||
});
|
||||
// Add custom headers for client information
|
||||
res.set({
|
||||
'X-RateLimit-Policy': 'See API documentation for rate limiting details',
|
||||
});
|
||||
|
||||
next();
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -251,42 +251,42 @@ function addRateLimitHeaders(req, res, next) {
|
|||
* @returns {Function} WebSocket rate limiter function
|
||||
*/
|
||||
function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) {
|
||||
const connections = new Map();
|
||||
const connections = new Map();
|
||||
|
||||
return (socket, next) => {
|
||||
const ip = socket.handshake.address;
|
||||
const now = Date.now();
|
||||
return (socket, next) => {
|
||||
const ip = socket.handshake.address;
|
||||
const now = Date.now();
|
||||
|
||||
// Clean up old connections
|
||||
if (connections.has(ip)) {
|
||||
const connectionTimes = connections.get(ip).filter(time => now - time < windowMs);
|
||||
connections.set(ip, connectionTimes);
|
||||
}
|
||||
// Clean up old connections
|
||||
if (connections.has(ip)) {
|
||||
const connectionTimes = connections.get(ip).filter(time => now - time < windowMs);
|
||||
connections.set(ip, connectionTimes);
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
const currentConnections = connections.get(ip) || [];
|
||||
if (currentConnections.length >= maxConnections) {
|
||||
logger.warn('WebSocket connection rate limit exceeded', {
|
||||
ip,
|
||||
currentConnections: currentConnections.length,
|
||||
maxConnections
|
||||
});
|
||||
// Check rate limit
|
||||
const currentConnections = connections.get(ip) || [];
|
||||
if (currentConnections.length >= maxConnections) {
|
||||
logger.warn('WebSocket connection rate limit exceeded', {
|
||||
ip,
|
||||
currentConnections: currentConnections.length,
|
||||
maxConnections,
|
||||
});
|
||||
|
||||
return next(new Error('Connection rate limit exceeded'));
|
||||
}
|
||||
return next(new Error('Connection rate limit exceeded'));
|
||||
}
|
||||
|
||||
// Add current connection
|
||||
currentConnections.push(now);
|
||||
connections.set(ip, currentConnections);
|
||||
// Add current connection
|
||||
currentConnections.push(now);
|
||||
connections.set(ip, currentConnections);
|
||||
|
||||
logger.debug('WebSocket connection allowed', {
|
||||
ip,
|
||||
connections: currentConnections.length,
|
||||
maxConnections
|
||||
});
|
||||
logger.debug('WebSocket connection allowed', {
|
||||
ip,
|
||||
connections: currentConnections.length,
|
||||
maxConnections,
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -296,25 +296,25 @@ function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) {
|
|||
* @param {Function} next - Express next function
|
||||
*/
|
||||
function dynamicRateLimit(req, res, next) {
|
||||
const userType = req.user?.type;
|
||||
|
||||
let limiter;
|
||||
if (userType === 'admin') {
|
||||
limiter = rateLimiters.admin;
|
||||
} else if (userType === 'player') {
|
||||
limiter = rateLimiters.player;
|
||||
} else {
|
||||
limiter = rateLimiters.global;
|
||||
}
|
||||
const userType = req.user?.type;
|
||||
|
||||
return limiter(req, res, next);
|
||||
let limiter;
|
||||
if (userType === 'admin') {
|
||||
limiter = rateLimiters.admin;
|
||||
} else if (userType === 'player') {
|
||||
limiter = rateLimiters.player;
|
||||
} else {
|
||||
limiter = rateLimiters.global;
|
||||
}
|
||||
|
||||
return limiter(req, res, next);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rateLimiters,
|
||||
createRateLimiter,
|
||||
createWebSocketRateLimiter,
|
||||
addRateLimitHeaders,
|
||||
dynamicRateLimit,
|
||||
RATE_LIMIT_CONFIG
|
||||
};
|
||||
rateLimiters,
|
||||
createRateLimiter,
|
||||
createWebSocketRateLimiter,
|
||||
addRateLimitHeaders,
|
||||
dynamicRateLimit,
|
||||
RATE_LIMIT_CONFIG,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue