Initial commit: Shattered Void MMO foundation

- Complete PostgreSQL database schema with 21+ tables
- Express.js server with dual authentication (player/admin)
- WebSocket support for real-time features
- Comprehensive middleware (auth, validation, logging, security)
- Game systems: colonies, resources, fleets, research, factions
- Plugin-based combat architecture
- Admin panel foundation
- Production-ready logging and error handling
- Docker support and CI/CD ready
- Complete project structure following CLAUDE.md patterns

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MegaProxy 2025-08-02 02:13:05 +00:00
commit 1a60cf55a3
69 changed files with 24471 additions and 0 deletions

47
.claude/agents/act.md Normal file
View file

@ -0,0 +1,47 @@
---
name: act
description: OODA Act phase - Implements the decided solution with precision, tests thoroughly, and validates results
tools: Read, Write, Edit, MultiEdit, Bash, Grep, Glob, LS, TodoWrite
---
You are the Act agent, responsible for the final phase of the OODA loop. Your role is to execute the chosen decision with precision, ensuring high-quality implementation and validation.
Your core responsibilities:
1. **Precise Implementation**: Execute the decided approach with attention to detail
2. **Code Quality**: Ensure clean, maintainable, and idiomatic code
3. **Testing**: Verify the implementation works correctly
4. **Validation**: Confirm the solution addresses the original problem
5. **Documentation**: Update relevant documentation if needed
Implementation approach:
- Follow the codebase's existing patterns and conventions
- Write clean, self-documenting code
- Handle edge cases and error conditions
- Ensure backward compatibility when applicable
- Minimize changes to achieve the goal
Quality checklist:
- Code follows project style guidelines
- All tests pass (run existing test suite)
- No linting or type-checking errors
- Changes are atomic and focused
- Error handling is robust
- Performance impact is acceptable
Validation steps:
1. Implement the solution incrementally
2. Test each change before proceeding
3. Run relevant test suites
4. Verify the fix addresses the original issue
5. Check for unintended side effects
6. Ensure no regression in existing functionality
Output format:
- Summary of changes made
- Files modified with specific changes
- Test results and validation outcomes
- Any issues encountered and how they were resolved
- Confirmation that the implementation is complete
- Next steps or follow-up actions if needed
Remember: Your role is execution with excellence. Be meticulous in implementation, thorough in testing, and clear in communication about what was done. Quality over speed, but maintain focus on delivering the decided solution.

41
.claude/agents/decide.md Normal file
View file

@ -0,0 +1,41 @@
---
name: decide
description: OODA Decide phase - Evaluates options, considers trade-offs, and recommends the best course of action
tools: Read, WebSearch, WebFetch
---
You are the Decide agent, responsible for the third phase of the OODA loop. Your role is to evaluate possible courses of action based on the observations and analysis, then recommend the best approach.
Your core responsibilities:
1. **Option Generation**: Identify multiple viable approaches to address the situation
2. **Trade-off Analysis**: Evaluate pros and cons of each option
3. **Risk Assessment**: Consider potential risks and mitigation strategies
4. **Feasibility Evaluation**: Assess technical complexity and resource requirements
5. **Recommendation Formation**: Select and justify the optimal approach
Decision-making framework:
- Generate at least 3 distinct options when possible
- Consider both immediate fixes and long-term solutions
- Evaluate impact on system architecture and maintainability
- Assess alignment with project conventions and best practices
- Consider time constraints and available resources
Evaluation criteria:
- Technical correctness and robustness
- Maintainability and code quality
- Performance implications
- Security considerations
- Alignment with existing patterns
- Implementation complexity
- Testing requirements
Output format:
- Clear problem statement based on analysis
- List of viable options with descriptions
- Comparative analysis of each option
- Recommended approach with justification
- Implementation strategy overview
- Potential risks and mitigation plans
- Success criteria for the chosen approach
Remember: Your role is to make informed decisions based on thorough analysis. Be decisive but transparent about trade-offs. Provide clear reasoning for your recommendations to enable effective action in the next phase.

29
.claude/agents/observe.md Normal file
View file

@ -0,0 +1,29 @@
---
name: observe
description: OODA Observe phase - Gathers comprehensive information about the current situation, codebase state, and problem context
tools: Read, Grep, Glob, LS, Bash, WebSearch, WebFetch
---
You are the Observe agent, responsible for the first phase of the OODA loop. Your primary role is to gather comprehensive, unbiased information about the current situation without making judgments or decisions.
Your core responsibilities:
1. **Information Gathering**: Systematically collect all relevant data about the problem, codebase, or situation
2. **Context Discovery**: Identify and document the broader context surrounding the issue
3. **Pattern Recognition**: Note recurring themes, structures, or anomalies in the observed data
4. **Comprehensive Coverage**: Ensure no critical information is overlooked
Approach to observation:
- Start with a broad scan, then narrow focus based on relevance
- Use multiple tools to cross-reference and validate findings
- Document raw observations without interpretation
- Capture both explicit information and implicit patterns
- Note what's present AND what's notably absent
Output format:
- Structured summary of all observations
- Key files, functions, and components identified
- Relevant patterns or anomalies discovered
- Potential areas requiring deeper investigation
- Raw data that may be useful for subsequent phases
Remember: Your role is purely observational. Avoid making conclusions, judgments, or recommendations. Simply gather and present the facts as comprehensively as possible for the next phase of the OODA loop.

38
.claude/agents/orient.md Normal file
View file

@ -0,0 +1,38 @@
---
name: orient
description: OODA Orient phase - Analyzes observations to understand context, identify patterns, and synthesize insights
tools: Read, Grep, Glob, WebSearch, WebFetch
---
You are the Orient agent, responsible for the second phase of the OODA loop. Your role is to analyze and make sense of the observations gathered in the previous phase, providing context and understanding.
Your core responsibilities:
1. **Contextual Analysis**: Place observations within the broader system context
2. **Pattern Synthesis**: Connect disparate observations to identify meaningful patterns
3. **Relationship Mapping**: Understand how different components interact and affect each other
4. **Priority Assessment**: Determine which observations are most critical or impactful
5. **Assumption Identification**: Recognize and document any assumptions or biases
Analytical approach:
- Apply domain knowledge to interpret raw observations
- Identify cause-and-effect relationships
- Recognize design patterns, architectural choices, and coding conventions
- Assess technical debt and potential risks
- Consider multiple perspectives and interpretations
Key questions to address:
- What do these observations mean in context?
- How do different pieces of information relate to each other?
- What patterns or anti-patterns are present?
- What are the root causes vs symptoms?
- What constraints or dependencies exist?
Output format:
- Synthesized understanding of the situation
- Key insights and their implications
- Identified patterns and relationships
- Critical factors affecting the problem
- Potential blind spots or areas of uncertainty
- Prioritized list of concerns or opportunities
Remember: Your role is analytical, not prescriptive. Focus on understanding and interpreting the observations to provide clarity and insight for decision-making in the next phase.

73
.env.example Normal file
View file

@ -0,0 +1,73 @@
# Shattered Void MMO - Environment Configuration
# Environment
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=shattered_void_dev
DB_USER=postgres
DB_PASSWORD=password
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# JWT Configuration
JWT_PLAYER_SECRET=player-secret-change-in-production
JWT_ADMIN_SECRET=admin-secret-change-in-production
JWT_REFRESH_SECRET=refresh-secret-change-in-production
JWT_PLAYER_EXPIRES_IN=24h
JWT_ADMIN_EXPIRES_IN=8h
JWT_REFRESH_EXPIRES_IN=7d
JWT_ISSUER=shattered-void-mmo
# Password Configuration
BCRYPT_SALT_ROUNDS=12
MIN_PASSWORD_LENGTH=8
MAX_PASSWORD_LENGTH=128
MIN_USERNAME_LENGTH=3
MAX_USERNAME_LENGTH=20
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=1000
DISABLE_RATE_LIMITING=false
# CORS Configuration
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000,http://127.0.0.1:3001
CORS_CREDENTIALS=true
CORS_MAX_AGE=86400
# WebSocket Configuration
WEBSOCKET_CORS_ORIGIN=http://localhost:3000,http://localhost:3001
WEBSOCKET_PING_TIMEOUT=20000
WEBSOCKET_PING_INTERVAL=25000
WEBSOCKET_MAX_CONNECTIONS=1000
WEBSOCKET_MAX_BUFFER_SIZE=1000000
# Game Configuration
STARTING_RESOURCES_SCRAP=1000
STARTING_RESOURCES_ENERGY=500
MAX_COLONIES_PER_PLAYER=10
ENABLE_GAME_TICK=false
# Feature Flags
ENABLE_ADMIN_ROUTES=true
ENABLE_DEBUG_ENDPOINTS=true
BLOCK_DISPOSABLE_EMAILS=false
# Email Configuration (optional)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=noreply@shattered-void.com
# Request Configuration
REQUEST_SIZE_LIMIT=10mb

102
.eslintrc.js Normal file
View file

@ -0,0 +1,102 @@
module.exports = {
env: {
browser: false,
commonjs: true,
es6: true,
node: true,
jest: true,
},
extends: [
'eslint:recommended',
'plugin:node/recommended',
'plugin:jest/recommended',
],
plugins: ['node', 'jest'],
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
// Code Quality
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-var': 'error',
'prefer-const': 'error',
'prefer-arrow-callback': 'error',
'arrow-spacing': 'error',
// Style
'indent': ['error', 2],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'comma-dangle': ['error', 'always-multiline'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
'space-before-blocks': 'error',
'keyword-spacing': 'error',
'space-infix-ops': 'error',
'eol-last': 'error',
'no-trailing-spaces': 'error',
'max-len': ['warn', { code: 120, ignoreComments: true }],
// Functions
'func-names': 'off',
'space-before-function-paren': ['error', {
anonymous: 'always',
named: 'never',
asyncArrow: 'always',
}],
// Objects
'object-shorthand': 'error',
'quote-props': ['error', 'as-needed'],
// Arrays
'array-callback-return': 'error',
// Error Handling
'handle-callback-err': 'error',
'no-throw-literal': 'error',
// Security
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
// Node.js specific
'node/no-unpublished-require': 'error',
'node/no-missing-require': 'error',
'node/no-deprecated-api': 'error',
'node/prefer-global/buffer': ['error', 'always'],
'node/prefer-global/console': ['error', 'always'],
'node/prefer-global/process': ['error', 'always'],
'node/prefer-promises/dns': 'error',
'node/prefer-promises/fs': 'error',
// Jest specific
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
'jest/valid-expect': 'error',
},
overrides: [
{
files: ['**/*.test.js', '**/*.spec.js'],
env: {
jest: true,
},
rules: {
'no-console': 'off',
},
},
{
files: ['src/database/migrations/**/*.js', 'src/database/seeds/**/*.js'],
rules: {
'node/no-unpublished-require': 'off',
},
},
],
};

151
.gitignore vendored Normal file
View file

@ -0,0 +1,151 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.local
.env.development
.env.test
.env.production
# Keep .env.example for reference
!.env.example
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Docker
.dockerignore
docker-compose.override.yml
# Database
*.sqlite
*.sqlite3
*.db
# Testing
coverage/
.coverage
.pytest_cache/
# Application specific
uploads/
public/uploads/
backup/
temp/
# Jest
coverage/
*.lcov
# Documentation generated files
docs/generated/
# PM2
.pm2/

685
CLAUDE.md Normal file
View file

@ -0,0 +1,685 @@
# Shattered Void - MMO Strategy Game Development Guide
## Project Overview
Shattered Void is a cross-platform text-based MMO strategy game set in a post-collapse galaxy. Players rebuild civilizations from ruins through colony management, fleet construction, exploration, and participation in dynamic galaxy-wide events.
## Development Stack
- **Backend**: Node.js with Express framework
- **Database**: PostgreSQL (primary) + Redis (caching/sessions)
- **Frontend**: React with responsive design
- **Real-time**: WebSocket connections
- **Authentication**: JWT-based with email verification
- **Deployment**: Docker containers with CI/CD pipeline
## Code Quality Standards
### JavaScript/Node.js Standards
```javascript
// Use strict ESLint configuration
// Prefer async/await over promises
// Use descriptive variable names
// Include comprehensive error handling
// Add JSDoc comments for all functions
/**
* Calculate resource production for a colony building
* @param {Object} building - Building instance with type and level
* @param {Object} modifiers - Colony and player modifiers
* @returns {number} Production rate per hour
*/
async function calculateBuildingProduction(building, modifiers) {
try {
const baseProduction = await getBuildingBaseProduction(building.type, building.level);
const totalModifier = 1 + (modifiers.colony || 0) + (modifiers.player || 0);
return Math.floor(baseProduction * totalModifier);
} catch (error) {
logger.error(`Production calculation failed for building ${building.id}:`, error);
throw new Error(`Failed to calculate production: ${error.message}`);
}
}
```
### Database Best Practices
```sql
-- Always use transactions for multi-table operations
-- Include proper indexes for performance
-- Use foreign key constraints for data integrity
-- Add audit columns (created_at, updated_at, created_by)
-- Use descriptive column names and comments
CREATE TABLE colonies (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
coordinates VARCHAR(20) NOT NULL, -- Format: "A3-91-X"
planet_type_id INTEGER REFERENCES planet_types(id),
population INTEGER DEFAULT 0 CHECK (population >= 0),
morale INTEGER DEFAULT 100 CHECK (morale BETWEEN 0 AND 100),
founded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(coordinates),
INDEX idx_colonies_player_id (player_id),
INDEX idx_colonies_coordinates (coordinates)
);
```
## Debugging Guidelines
### Comprehensive Logging Strategy
```javascript
// Use structured logging with context
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
// Add request correlation IDs for tracing
app.use((req, res, next) => {
req.correlationId = require('uuid').v4();
logger.info('Request started', {
correlationId: req.correlationId,
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip
});
next();
});
```
### Error Handling Patterns
```javascript
// Service layer error handling
class ColonyService {
async createColony(playerId, colonyData) {
const correlationId = require('async_hooks').executionAsyncResource?.correlationId;
try {
logger.info('Creating colony', {
correlationId,
playerId,
coordinates: colonyData.coordinates
});
// Validate input data
if (!colonyData.coordinates || !this.isValidCoordinates(colonyData.coordinates)) {
throw new ValidationError('Invalid coordinates format');
}
// Check if coordinates are already taken
const existingColony = await this.getColonyByCoordinates(colonyData.coordinates);
if (existingColony) {
throw new ConflictError('Coordinates already occupied');
}
// Database transaction for atomic operation
const colony = await db.transaction(async (trx) => {
const [newColony] = await trx('colonies')
.insert({
player_id: playerId,
name: colonyData.name,
coordinates: colonyData.coordinates,
planet_type_id: colonyData.planet_type_id
})
.returning('*');
// Create initial buildings
await this.createInitialBuildings(newColony.id, trx);
logger.info('Colony created successfully', {
correlationId,
colonyId: newColony.id,
playerId
});
return newColony;
});
return colony;
} catch (error) {
logger.error('Colony creation failed', {
correlationId,
playerId,
coordinates: colonyData.coordinates,
error: error.message,
stack: error.stack
});
// Re-throw with context
if (error instanceof ValidationError || error instanceof ConflictError) {
throw error;
}
throw new ServiceError('Failed to create colony', error);
}
}
}
// Custom error classes for better error handling
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
}
}
class ConflictError extends Error {
constructor(message) {
super(message);
this.name = 'ConflictError';
this.statusCode = 409;
}
}
class ServiceError extends Error {
constructor(message, originalError) {
super(message);
this.name = 'ServiceError';
this.statusCode = 500;
this.originalError = originalError;
}
}
```
### Development Debugging Tools
```javascript
// Debug middleware for development
if (process.env.NODE_ENV === 'development') {
app.use('/debug', require('./middleware/debug'));
}
// Debug endpoint for inspecting game state
app.get('/debug/player/:playerId/state', async (req, res) => {
if (process.env.NODE_ENV !== 'development') {
return res.status(404).send('Not found');
}
try {
const playerId = req.params.playerId;
const playerState = {
player: await Player.findById(playerId),
colonies: await Colony.findByPlayerId(playerId),
fleets: await Fleet.findByPlayerId(playerId),
research: await Research.findByPlayerId(playerId),
resources: await Resource.findByPlayerId(playerId)
};
res.json({
debug: true,
timestamp: new Date().toISOString(),
playerState
});
} catch (error) {
logger.error('Debug endpoint error:', error);
res.status(500).json({ error: error.message });
}
});
// Performance monitoring
const performanceTracker = {
trackOperation: (operationName) => {
const start = process.hrtime.bigint();
return {
end: () => {
const duration = Number(process.hrtime.bigint() - start) / 1000000; // Convert to ms
logger.info('Operation completed', {
operation: operationName,
duration: `${duration.toFixed(2)}ms`
});
return duration;
}
};
}
};
// Usage in service methods
async function processGameTick() {
const tracker = performanceTracker.trackOperation('game_tick');
try {
await Promise.all([
processResourceProduction(),
processFleetMovements(),
processResearchCompletion(),
processBuildingConstruction()
]);
} finally {
tracker.end();
}
}
```
## Testing Strategy
### Unit Testing with Jest
```javascript
// colony.service.test.js
const ColonyService = require('../services/colony.service');
const db = require('../database/connection');
describe('ColonyService', () => {
let colonyService;
beforeEach(() => {
colonyService = new ColonyService();
// Mock database calls
jest.clearAllMocks();
});
describe('createColony', () => {
it('should create colony with valid data', async () => {
const playerId = 1;
const colonyData = {
name: 'Test Colony',
coordinates: 'A3-91-X',
planet_type_id: 1
};
// Mock database responses
db.transaction = jest.fn().mockImplementation(async (callback) => {
const mockTrx = {
'colonies': {
insert: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([{
id: 123,
player_id: playerId,
...colonyData
}])
})
}
};
return callback(mockTrx);
});
colonyService.getColonyByCoordinates = jest.fn().mockResolvedValue(null);
colonyService.createInitialBuildings = jest.fn().mockResolvedValue();
const result = await colonyService.createColony(playerId, colonyData);
expect(result).toHaveProperty('id', 123);
expect(result).toHaveProperty('name', 'Test Colony');
expect(colonyService.createInitialBuildings).toHaveBeenCalledWith(123, expect.any(Object));
});
it('should throw ConflictError for occupied coordinates', async () => {
const playerId = 1;
const colonyData = {
name: 'Test Colony',
coordinates: 'A3-91-X',
planet_type_id: 1
};
colonyService.getColonyByCoordinates = jest.fn().mockResolvedValue({
id: 456,
coordinates: 'A3-91-X'
});
await expect(colonyService.createColony(playerId, colonyData))
.rejects
.toThrow(ConflictError);
});
it('should throw ValidationError for invalid coordinates', async () => {
const playerId = 1;
const colonyData = {
name: 'Test Colony',
coordinates: 'INVALID',
planet_type_id: 1
};
colonyService.isValidCoordinates = jest.fn().mockReturnValue(false);
await expect(colonyService.createColony(playerId, colonyData))
.rejects
.toThrow(ValidationError);
});
});
});
```
### Integration Testing
```javascript
// integration/colony.integration.test.js
const request = require('supertest');
const app = require('../app');
const db = require('../database/connection');
describe('Colony Integration Tests', () => {
let authToken;
let testPlayer;
beforeAll(async () => {
// Set up test database
await db.migrate.latest();
await db.seed.run();
// Create test player and get auth token
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'testpassword123',
username: 'testplayer'
});
authToken = response.body.token;
testPlayer = response.body.player;
});
afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
// Clean up colonies before each test
await db('colonies').where('player_id', testPlayer.id).del();
});
it('should create colony via API endpoint', async () => {
const colonyData = {
name: 'Integration Test Colony',
coordinates: 'Z9-99-Z',
planet_type_id: 1
};
const response = await request(app)
.post('/api/colonies')
.set('Authorization', `Bearer ${authToken}`)
.send(colonyData)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(colonyData.name);
expect(response.body.coordinates).toBe(colonyData.coordinates);
// Verify in database
const dbColony = await db('colonies')
.where('id', response.body.id)
.first();
expect(dbColony).toHaveProperty('player_id', testPlayer.id);
});
it('should return 409 for duplicate coordinates', async () => {
const colonyData = {
name: 'First Colony',
coordinates: 'Z9-99-Y',
planet_type_id: 1
};
// Create first colony
await request(app)
.post('/api/colonies')
.set('Authorization', `Bearer ${authToken}`)
.send(colonyData)
.expect(201);
// Try to create second colony with same coordinates
const duplicateData = {
name: 'Duplicate Colony',
coordinates: 'Z9-99-Y',
planet_type_id: 1
};
const response = await request(app)
.post('/api/colonies')
.set('Authorization', `Bearer ${authToken}`)
.send(duplicateData)
.expect(409);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toContain('occupied');
});
});
```
## Development Commands
### Essential Scripts
```json
{
"scripts": {
"dev": "nodemon --inspect=0.0.0.0:9229 src/server.js",
"start": "node src/server.js",
"test": "jest --verbose --coverage",
"test:watch": "jest --watch --verbose",
"test:integration": "jest --testPathPattern=integration --runInBand",
"lint": "eslint src/ --ext .js --fix",
"lint:check": "eslint src/ --ext .js",
"typecheck": "jsdoc -t node_modules/tsd-jsdoc/dist -r src/ -d temp/ && rm -rf temp/",
"db:migrate": "knex migrate:latest",
"db:rollback": "knex migrate:rollback",
"db:seed": "knex seed:run",
"db:reset": "knex migrate:rollback:all && knex migrate:latest && knex seed:run",
"docker:build": "docker build -t shattered-void .",
"docker:run": "docker-compose up -d",
"logs": "tail -f logs/combined.log",
"logs:error": "tail -f logs/error.log"
}
}
```
### Environment Configuration
```bash
# .env.development
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=shattered_void_dev
DB_USER=postgres
DB_PASSWORD=password
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# JWT
JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRE=24h
# Game Configuration
GAME_TICK_INTERVAL=60000
MAX_COLONIES_PER_PLAYER=10
STARTING_RESOURCES_SCRAP=1000
STARTING_RESOURCES_ENERGY=500
# Debug Features
ENABLE_DEBUG_ENDPOINTS=true
ENABLE_SQL_LOGGING=true
```
## Performance Monitoring
### Database Query Optimization
```javascript
// Monitor slow queries
const slowQueryThreshold = 100; // ms
db.on('query', (queryData) => {
const startTime = Date.now();
queryData.response = queryData.response || {};
const originalCallback = queryData.response.callback;
queryData.response.callback = function(...args) {
const duration = Date.now() - startTime;
if (duration > slowQueryThreshold) {
logger.warn('Slow query detected', {
query: queryData.sql,
bindings: queryData.bindings,
duration: `${duration}ms`
});
}
if (originalCallback) {
originalCallback.apply(this, args);
}
};
});
// Add query performance metrics
const queryMetrics = new Map();
function trackQuery(queryName, query) {
return async (...args) => {
const start = process.hrtime.bigint();
try {
const result = await query(...args);
const duration = Number(process.hrtime.bigint() - start) / 1000000;
// Update metrics
const metrics = queryMetrics.get(queryName) || { count: 0, totalTime: 0, maxTime: 0 };
metrics.count++;
metrics.totalTime += duration;
metrics.maxTime = Math.max(metrics.maxTime, duration);
queryMetrics.set(queryName, metrics);
return result;
} catch (error) {
logger.error(`Query failed: ${queryName}`, { error: error.message });
throw error;
}
};
}
```
### Memory and Resource Monitoring
```javascript
// Memory usage tracking
setInterval(() => {
const memUsage = process.memoryUsage();
logger.info('Memory usage', {
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(memUsage.external / 1024 / 1024)}MB`
});
}, 60000); // Log every minute
// Database connection monitoring
setInterval(async () => {
try {
const dbStats = await db.raw('SELECT COUNT(*) as active_connections FROM pg_stat_activity WHERE state = ?', ['active']);
logger.info('Database connections', {
active: dbStats.rows[0].active_connections
});
} catch (error) {
logger.error('Failed to get DB connection stats:', error);
}
}, 30000);
```
## Security Best Practices
### Input Validation
```javascript
const Joi = require('joi');
// Validation schemas
const schemas = {
createColony: Joi.object({
name: Joi.string().min(3).max(50).required(),
coordinates: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(),
planet_type_id: Joi.number().integer().min(1).required()
}),
createFleet: Joi.object({
name: Joi.string().min(3).max(50).required(),
ships: Joi.array().items(
Joi.object({
design_id: Joi.number().integer().min(1).required(),
quantity: Joi.number().integer().min(1).max(100).required()
})
).min(1).required()
})
};
// Validation middleware
function validateRequest(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body);
if (error) {
logger.warn('Validation failed', {
correlationId: req.correlationId,
error: error.details[0].message,
body: req.body
});
return res.status(400).json({
error: 'Validation failed',
details: error.details[0].message
});
}
req.body = value; // Use sanitized data
next();
};
}
// Rate limiting
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', apiLimiter);
```
## File Structure
```
src/
├── controllers/ # Route handlers
├── services/ # Business logic
├── models/ # Database models
├── middleware/ # Express middleware
├── database/
│ ├── migrations/ # Database schema changes
│ ├── seeds/ # Test data
│ └── connection.js # Database configuration
├── utils/ # Helper functions
├── validators/ # Input validation schemas
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── fixtures/ # Test data
├── config/ # Configuration files
└── server.js # Application entry point
logs/ # Log files
docker/ # Docker configuration
docs/ # Documentation
scripts/ # Deployment and utility scripts
```
## Git Workflow
- Use feature branches: `feature/colony-management`
- Commit messages: `feat: add colony creation API endpoint`
- Always run `npm run lint && npm test` before committing
- Use conventional commits for automated changelog generation
This development guide ensures high code quality, comprehensive debugging capabilities, and maintainable architecture for the Shattered Void MMO project.

204
README.md Normal file
View file

@ -0,0 +1,204 @@
# Shattered Void MMO
A post-collapse galaxy MMO strategy game built with Node.js, Express, PostgreSQL, and WebSockets.
## 🎮 Game Overview
Shattered Void is a text-based MMO strategy game set in a post-collapse galaxy where players rebuild civilizations from ruins through colony management, fleet construction, exploration, and participation in dynamic galaxy-wide events.
### Core Features
- **Colony Management**: Build and upgrade colonies across different planet types
- **Fleet Construction**: Design modular ships and manage fleets
- **Research System**: Unlock new technologies and capabilities
- **Dynamic Events**: Participate in admin-driven and player-triggered galaxy events
- **Faction Warfare**: Form alliances and engage in diplomacy
- **Real-time Updates**: WebSocket-powered live game state updates
## 🏗️ Architecture
### Tech Stack
- **Backend**: Node.js with Express framework
- **Database**: PostgreSQL (primary) + Redis (caching/sessions)
- **Real-time**: WebSocket connections for live updates
- **Authentication**: JWT-based with separate admin/player systems
- **Testing**: Jest with comprehensive unit and integration tests
### Project Structure
```
src/
├── config/ # Database, Redis, logging configuration
├── database/ # Migrations, seeds, and query files
├── models/ # Database models organized by domain
├── services/ # Business logic layer
├── controllers/ # HTTP route handlers (API + Admin)
├── middleware/ # Express middleware (auth, validation, etc.)
├── validators/ # Input validation schemas
├── utils/ # Helper functions and utilities
├── plugins/ # Plugin system for extensible game mechanics
├── jobs/ # Background job processing
└── routes/ # Route definitions
tests/
├── unit/ # Unit tests for services, models, utils
├── integration/ # API and database integration tests
├── e2e/ # End-to-end tests
├── fixtures/ # Test data
└── helpers/ # Test utilities
```
## 🚀 Getting Started
### Prerequisites
- Node.js 18+ and npm 9+
- PostgreSQL 14+
- Redis 6+
### Installation
1. Clone the repository
2. Copy environment configuration:
```bash
cp .env.example .env.development
```
3. Update `.env.development` with your database credentials
4. Install dependencies:
```bash
npm install
```
5. Set up the database:
```bash
npm run db:setup
```
6. Start the development server:
```bash
npm run dev
```
The server will start on `http://localhost:3000` with WebSocket support.
## 🎯 Game Systems
### Resource Economy
- **Scrap Metal**: Basic building materials
- **Energy Cells**: Power for operations
- **Data Cores**: Advanced technology storage
- **Rare Elements**: Exotic materials for advanced construction
### Colony Management
- Build on 6 different planet types (Terran, Industrial, Research, Mining, Fortress, Derelict)
- Construct 8+ building types with upgrade paths
- Manage population, morale, and defense
### Fleet Operations
- Design custom ships with modular components
- Move fleets across galaxy coordinates (A3-91-X format)
- Engage in instant combat with detailed battle logs
### Research & Technology
- Unlock technologies across 4 categories: Military, Industrial, Social, Exploration
- Build research facilities to accelerate progress
- Prerequisites system for complex tech trees
## 🔧 Development
### Available Scripts
- `npm run dev` - Start development server with hot reload
- `npm run test` - Run test suite with coverage
- `npm run test:watch` - Run tests in watch mode
- `npm run lint` - Run ESLint code quality checks
- `npm run db:migrate` - Run database migrations
- `npm run db:seed` - Populate database with initial data
- `npm run db:reset` - Reset database to clean state
### Code Quality
The project enforces strict code quality standards:
- ESLint configuration with Node.js and Jest rules
- Comprehensive error handling with custom error classes
- Structured logging with Winston and correlation IDs
- Input validation with Joi schemas
- Database transactions for data integrity
### Testing Strategy
- **Unit Tests**: Individual functions and modules
- **Integration Tests**: API endpoints and database operations
- **E2E Tests**: Complete user workflows
- Coverage target: 80%+ for critical game systems
## 🎮 Game Tick System
The game runs on a configurable tick system:
- Default: 60-second intervals
- User groups: Players distributed across 10 processing groups
- Retry logic: 5 attempts with bonus tick compensation
- Real-time monitoring for administrators
## 🔌 Plugin Architecture
Extensible plugin system for game mechanics:
- Combat resolution plugins
- Event system plugins
- Resource calculation plugins
- Custom game rule implementations
## 📊 Admin Features
- Real-time game statistics dashboard
- Event creation and management tools
- Player administration and support
- System configuration with hot-reloading
- Performance monitoring and alerts
## 🔒 Security
- JWT authentication with separate admin/player tokens
- Rate limiting and CORS protection
- Input validation and SQL injection prevention
- Audit logging for all critical operations
- Environment-based configuration management
## 📈 Scalability
Designed to support 100-1000+ concurrent players:
- Connection pooling and query optimization
- Redis caching for frequently accessed data
- User group distribution for tick processing
- Comprehensive indexing strategy
- Performance metrics collection
## 🐳 Deployment
Docker support included:
- Development and production containers
- Docker Compose configurations
- Environment-specific builds
- Automated CI/CD pipeline ready
## 📝 API Documentation
- **Player API**: Authentication, colonies, fleets, research, events
- **Admin API**: System management, player administration, analytics
- **WebSocket Events**: Real-time game state updates
- Full API documentation available in `/docs/api/`
## 🤝 Contributing
1. Follow the existing code style and patterns
2. Write comprehensive tests for new features
3. Update documentation for API changes
4. Use conventional commit messages
5. Run `npm run lint && npm test` before committing
## 📄 License
Private - All rights reserved
---
Built with ❤️ for the post-collapse galaxy simulation community

637
database-schema.sql Normal file
View file

@ -0,0 +1,637 @@
-- Shattered Void MMO - Comprehensive Database Schema
-- This schema supports all game systems with flexibility for future expansion
-- ===== CORE SYSTEM TABLES =====
-- System configuration with hot-reloading support
CREATE TABLE system_config (
id SERIAL PRIMARY KEY,
config_key VARCHAR(100) UNIQUE NOT NULL,
config_value JSONB NOT NULL,
config_type VARCHAR(20) NOT NULL CHECK (config_type IN ('string', 'number', 'boolean', 'json', 'array')),
description TEXT,
requires_restart BOOLEAN DEFAULT false,
is_public BOOLEAN DEFAULT false, -- Can be exposed to client
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_by INTEGER -- Will reference admin_users(id) after table creation
);
-- Game tick system with user grouping
CREATE TABLE game_tick_config (
id SERIAL PRIMARY KEY,
tick_interval_ms INTEGER NOT NULL DEFAULT 60000,
user_groups_count INTEGER NOT NULL DEFAULT 10,
max_retry_attempts INTEGER NOT NULL DEFAULT 5,
bonus_tick_threshold INTEGER NOT NULL DEFAULT 3,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE game_tick_log (
id BIGSERIAL PRIMARY KEY,
tick_number BIGINT NOT NULL,
user_group INTEGER NOT NULL,
started_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP,
status VARCHAR(20) NOT NULL CHECK (status IN ('running', 'completed', 'failed', 'retrying')),
retry_count INTEGER DEFAULT 0,
error_message TEXT,
processed_players INTEGER DEFAULT 0,
performance_metrics JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Event system configuration
CREATE TABLE event_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
trigger_type VARCHAR(20) NOT NULL CHECK (trigger_type IN ('admin', 'player', 'system', 'mixed')),
is_active BOOLEAN DEFAULT true,
config_schema JSONB, -- JSON schema for event configuration
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE event_instances (
id BIGSERIAL PRIMARY KEY,
event_type_id INTEGER NOT NULL REFERENCES event_types(id),
name VARCHAR(200) NOT NULL,
description TEXT,
config JSONB NOT NULL,
start_time TIMESTAMP,
end_time TIMESTAMP,
status VARCHAR(20) NOT NULL CHECK (status IN ('scheduled', 'active', 'completed', 'cancelled')),
created_by INTEGER, -- Will reference admin_users(id) after table creation
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Plugin system for extensibility
CREATE TABLE plugins (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
version VARCHAR(20) NOT NULL,
description TEXT,
plugin_type VARCHAR(50) NOT NULL, -- 'combat', 'event', 'resource', etc.
is_active BOOLEAN DEFAULT false,
config JSONB,
dependencies JSONB, -- Array of required plugins
hooks JSONB, -- Available hook points
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ===== USER MANAGEMENT =====
-- Admin users with role-based access
CREATE TABLE admin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'moderator',
permissions JSONB, -- Specific permissions array
is_active BOOLEAN DEFAULT true,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Player accounts
CREATE TABLE players (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT false,
email_verification_token VARCHAR(255),
reset_password_token VARCHAR(255),
reset_password_expires TIMESTAMP,
user_group INTEGER NOT NULL CHECK (user_group >= 0 AND user_group < 10),
is_active BOOLEAN DEFAULT true,
is_banned BOOLEAN DEFAULT false,
ban_reason TEXT,
ban_expires TIMESTAMP,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Player preferences and game settings
CREATE TABLE player_settings (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
setting_key VARCHAR(100) NOT NULL,
setting_value JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(player_id, setting_key)
);
-- WebSocket subscriptions for real-time updates
CREATE TABLE player_subscriptions (
id BIGSERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
subscription_type VARCHAR(50) NOT NULL, -- 'colony', 'fleet', 'battle', 'event', etc.
resource_id INTEGER, -- Specific resource ID (colony_id, fleet_id, etc.)
filters JSONB, -- Additional filtering criteria
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP
);
-- ===== GALAXY & COLONIES =====
-- Planet types with generation rules
CREATE TABLE planet_types (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
base_resources JSONB NOT NULL, -- Starting resource deposits
resource_modifiers JSONB, -- Production modifiers by resource type
max_population INTEGER,
special_features JSONB, -- Array of special features
rarity_weight INTEGER DEFAULT 100, -- For random generation
is_active BOOLEAN DEFAULT true
);
-- Galaxy sectors for organization
CREATE TABLE galaxy_sectors (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
coordinates VARCHAR(10) UNIQUE NOT NULL, -- e.g., "A3"
description TEXT,
danger_level INTEGER DEFAULT 1 CHECK (danger_level BETWEEN 1 AND 10),
special_rules JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Player colonies
CREATE TABLE colonies (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
coordinates VARCHAR(20) UNIQUE NOT NULL, -- Format: "A3-91-X"
sector_id INTEGER REFERENCES galaxy_sectors(id),
planet_type_id INTEGER NOT NULL REFERENCES planet_types(id),
population INTEGER DEFAULT 0 CHECK (population >= 0),
max_population INTEGER DEFAULT 1000,
morale INTEGER DEFAULT 100 CHECK (morale BETWEEN 0 AND 100),
loyalty INTEGER DEFAULT 100 CHECK (loyalty BETWEEN 0 AND 100),
founded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Building types with upgrade paths
CREATE TABLE building_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
category VARCHAR(50) NOT NULL, -- 'production', 'military', 'research', 'infrastructure'
max_level INTEGER DEFAULT 10,
base_cost JSONB NOT NULL, -- Resource costs for level 1
cost_multiplier DECIMAL(3,2) DEFAULT 1.5, -- Cost increase per level
base_production JSONB, -- Resource production at level 1
production_multiplier DECIMAL(3,2) DEFAULT 1.2, -- Production increase per level
prerequisites JSONB, -- Required buildings/research
special_effects JSONB, -- Special abilities or bonuses
is_unique BOOLEAN DEFAULT false, -- Only one per colony
is_active BOOLEAN DEFAULT true
);
-- Colony buildings
CREATE TABLE colony_buildings (
id SERIAL PRIMARY KEY,
colony_id INTEGER NOT NULL REFERENCES colonies(id) ON DELETE CASCADE,
building_type_id INTEGER NOT NULL REFERENCES building_types(id),
level INTEGER DEFAULT 1 CHECK (level > 0),
health_percentage INTEGER DEFAULT 100 CHECK (health_percentage BETWEEN 0 AND 100),
is_under_construction BOOLEAN DEFAULT false,
construction_started TIMESTAMP,
construction_completes TIMESTAMP,
last_production TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(colony_id, building_type_id)
);
-- ===== RESOURCES & ECONOMY =====
-- Resource types
CREATE TABLE resource_types (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
category VARCHAR(30) NOT NULL, -- 'basic', 'advanced', 'rare', 'currency'
max_storage INTEGER, -- NULL for unlimited
decay_rate DECIMAL(5,4), -- Daily decay percentage
trade_value DECIMAL(10,2), -- Base trade value
is_tradeable BOOLEAN DEFAULT true,
is_active BOOLEAN DEFAULT true
);
-- Player resource stockpiles
CREATE TABLE player_resources (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
resource_type_id INTEGER NOT NULL REFERENCES resource_types(id),
amount BIGINT DEFAULT 0 CHECK (amount >= 0),
storage_capacity BIGINT,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(player_id, resource_type_id)
);
-- Colony resource production/consumption tracking
CREATE TABLE colony_resource_production (
id BIGSERIAL PRIMARY KEY,
colony_id INTEGER NOT NULL REFERENCES colonies(id) ON DELETE CASCADE,
resource_type_id INTEGER NOT NULL REFERENCES resource_types(id),
production_rate INTEGER DEFAULT 0, -- Per hour
consumption_rate INTEGER DEFAULT 0, -- Per hour
current_stored BIGINT DEFAULT 0,
storage_capacity BIGINT,
last_calculated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(colony_id, resource_type_id)
);
-- Trade system
CREATE TABLE trade_routes (
id SERIAL PRIMARY KEY,
from_colony_id INTEGER NOT NULL REFERENCES colonies(id) ON DELETE CASCADE,
to_colony_id INTEGER NOT NULL REFERENCES colonies(id) ON DELETE CASCADE,
resource_type_id INTEGER NOT NULL REFERENCES resource_types(id),
amount_per_trip INTEGER NOT NULL,
price_per_unit DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'paused', 'cancelled')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_different_colonies CHECK (from_colony_id != to_colony_id)
);
-- ===== FLEET & COMBAT =====
-- Ship design system
CREATE TABLE ship_designs (
id SERIAL PRIMARY KEY,
player_id INTEGER REFERENCES players(id) ON DELETE CASCADE, -- NULL for standard designs
name VARCHAR(100) NOT NULL,
ship_class VARCHAR(50) NOT NULL, -- 'fighter', 'corvette', 'destroyer', 'cruiser', 'battleship'
hull_type VARCHAR(50) NOT NULL,
components JSONB NOT NULL, -- Weapon, shield, engine configurations
stats JSONB NOT NULL, -- Calculated stats: hp, attack, defense, speed, etc.
cost JSONB NOT NULL, -- Resource cost to build
build_time INTEGER NOT NULL, -- In minutes
is_public BOOLEAN DEFAULT false, -- Available to all players
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Player fleets
CREATE TABLE fleets (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
current_location VARCHAR(20) NOT NULL, -- Coordinates
destination VARCHAR(20), -- If moving
fleet_status VARCHAR(20) DEFAULT 'idle' CHECK (fleet_status IN ('idle', 'moving', 'in_combat', 'constructing', 'repairing')),
movement_started TIMESTAMP,
arrival_time TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Ships in fleets
CREATE TABLE fleet_ships (
id SERIAL PRIMARY KEY,
fleet_id INTEGER NOT NULL REFERENCES fleets(id) ON DELETE CASCADE,
ship_design_id INTEGER NOT NULL REFERENCES ship_designs(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
health_percentage INTEGER DEFAULT 100 CHECK (health_percentage BETWEEN 0 AND 100),
experience INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Combat system with plugin support
CREATE TABLE combat_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
plugin_name VARCHAR(100), -- References plugins table
config JSONB,
is_active BOOLEAN DEFAULT true
);
CREATE TABLE battles (
id BIGSERIAL PRIMARY KEY,
battle_type VARCHAR(50) NOT NULL,
location VARCHAR(20) NOT NULL,
combat_type_id INTEGER REFERENCES combat_types(id),
participants JSONB NOT NULL, -- Array of fleet/player IDs
status VARCHAR(20) DEFAULT 'preparing' CHECK (status IN ('preparing', 'active', 'completed', 'cancelled')),
battle_data JSONB, -- Combat calculations and state
result JSONB, -- Winner, casualties, loot, etc.
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ===== RESEARCH & TECHNOLOGY =====
-- Technology tree
CREATE TABLE technologies (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
category VARCHAR(50) NOT NULL, -- 'military', 'industrial', 'social', 'exploration'
tier INTEGER NOT NULL DEFAULT 1 CHECK (tier > 0),
prerequisites JSONB, -- Array of required technology IDs
research_cost JSONB NOT NULL, -- Resource costs
research_time INTEGER NOT NULL, -- In minutes
effects JSONB, -- Bonuses, unlocks, etc.
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Player research progress
CREATE TABLE player_research (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
technology_id INTEGER NOT NULL REFERENCES technologies(id),
status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('unavailable', 'available', 'researching', 'completed')),
progress INTEGER DEFAULT 0 CHECK (progress >= 0),
started_at TIMESTAMP,
completed_at TIMESTAMP,
UNIQUE(player_id, technology_id)
);
-- Research labs and facilities
CREATE TABLE research_facilities (
id SERIAL PRIMARY KEY,
colony_id INTEGER NOT NULL REFERENCES colonies(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
facility_type VARCHAR(50) NOT NULL,
research_bonus DECIMAL(3,2) DEFAULT 1.0, -- Multiplier for research speed
specialization JSONB, -- Categories this facility is good at
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ===== FACTIONS & DIPLOMACY =====
-- Player factions/alliances
CREATE TABLE factions (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
tag VARCHAR(10) UNIQUE NOT NULL, -- Short identifier
leader_id INTEGER REFERENCES players(id),
faction_type VARCHAR(20) DEFAULT 'alliance' CHECK (faction_type IN ('alliance', 'corporation', 'empire')),
is_recruiting BOOLEAN DEFAULT true,
requirements JSONB, -- Join requirements
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Faction membership
CREATE TABLE faction_members (
id SERIAL PRIMARY KEY,
faction_id INTEGER NOT NULL REFERENCES factions(id) ON DELETE CASCADE,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
rank VARCHAR(50) DEFAULT 'member',
permissions JSONB, -- What they can do
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
contribution_score INTEGER DEFAULT 0,
UNIQUE(faction_id, player_id)
);
-- Diplomatic relations
CREATE TABLE diplomatic_relations (
id SERIAL PRIMARY KEY,
faction_a_id INTEGER NOT NULL REFERENCES factions(id) ON DELETE CASCADE,
faction_b_id INTEGER NOT NULL REFERENCES factions(id) ON DELETE CASCADE,
relation_type VARCHAR(20) NOT NULL CHECK (relation_type IN ('allied', 'friendly', 'neutral', 'hostile', 'war')),
established_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
established_by INTEGER REFERENCES players(id),
CONSTRAINT check_different_factions CHECK (faction_a_id != faction_b_id),
UNIQUE(faction_a_id, faction_b_id)
);
-- ===== AUDIT & STATISTICS =====
-- Comprehensive audit log
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, -- 'player', 'colony', 'fleet', 'battle', etc.
entity_id INTEGER NOT NULL,
action VARCHAR(100) NOT NULL,
actor_type VARCHAR(20) NOT NULL CHECK (actor_type IN ('player', 'admin', 'system')),
actor_id INTEGER,
changes JSONB, -- Before/after values
metadata JSONB, -- Additional context
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Data retention policy
CREATE TABLE data_retention_policies (
id SERIAL PRIMARY KEY,
table_name VARCHAR(100) NOT NULL,
retention_days INTEGER NOT NULL DEFAULT 30,
archive_before_delete BOOLEAN DEFAULT true,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(table_name)
);
-- Game statistics for analysis
CREATE TABLE game_statistics (
id BIGSERIAL PRIMARY KEY,
stat_type VARCHAR(100) NOT NULL,
stat_key VARCHAR(200) NOT NULL,
stat_value DECIMAL(15,4) NOT NULL,
dimensions JSONB, -- Additional grouping dimensions
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Performance metrics
CREATE TABLE performance_metrics (
id BIGSERIAL PRIMARY KEY,
metric_name VARCHAR(100) NOT NULL,
metric_value DECIMAL(10,4) NOT NULL,
tags JSONB, -- Additional metadata
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ===== INDEXES FOR PERFORMANCE =====
-- User management indexes
CREATE INDEX idx_players_user_group ON players(user_group);
CREATE INDEX idx_players_email ON players(email);
CREATE INDEX idx_players_username ON players(username);
CREATE INDEX idx_player_subscriptions_player_id ON player_subscriptions(player_id);
CREATE INDEX idx_player_subscriptions_type ON player_subscriptions(subscription_type);
CREATE INDEX idx_player_subscriptions_expires ON player_subscriptions(expires_at);
-- Colony and building indexes
CREATE INDEX idx_colonies_player_id ON colonies(player_id);
CREATE INDEX idx_colonies_coordinates ON colonies(coordinates);
CREATE INDEX idx_colonies_sector ON colonies(sector_id);
CREATE INDEX idx_colony_buildings_colony_id ON colony_buildings(colony_id);
CREATE INDEX idx_colony_buildings_construction ON colony_buildings(construction_completes) WHERE is_under_construction = true;
-- Resource indexes
CREATE INDEX idx_player_resources_player_id ON player_resources(player_id);
CREATE INDEX idx_colony_production_colony_id ON colony_resource_production(colony_id);
CREATE INDEX idx_trade_routes_from_colony ON trade_routes(from_colony_id);
CREATE INDEX idx_trade_routes_to_colony ON trade_routes(to_colony_id);
-- Fleet and combat indexes
CREATE INDEX idx_ship_designs_player_id ON ship_designs(player_id);
CREATE INDEX idx_ship_designs_class ON ship_designs(ship_class);
CREATE INDEX idx_fleets_player_id ON fleets(player_id);
CREATE INDEX idx_fleets_location ON fleets(current_location);
CREATE INDEX idx_fleets_arrival ON fleets(arrival_time) WHERE arrival_time IS NOT NULL;
CREATE INDEX idx_fleet_ships_fleet_id ON fleet_ships(fleet_id);
CREATE INDEX idx_battles_location ON battles(location);
CREATE INDEX idx_battles_status ON battles(status);
CREATE INDEX idx_battles_completed ON battles(completed_at);
-- Research indexes
CREATE INDEX idx_player_research_player_id ON player_research(player_id);
CREATE INDEX idx_player_research_status ON player_research(status);
CREATE INDEX idx_research_facilities_colony_id ON research_facilities(colony_id);
-- Faction indexes
CREATE INDEX idx_factions_leader_id ON factions(leader_id);
CREATE INDEX idx_faction_members_faction_id ON faction_members(faction_id);
CREATE INDEX idx_faction_members_player_id ON faction_members(player_id);
CREATE INDEX idx_diplomatic_relations_faction_a ON diplomatic_relations(faction_a_id);
CREATE INDEX idx_diplomatic_relations_faction_b ON diplomatic_relations(faction_b_id);
-- Audit and statistics indexes
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
CREATE INDEX idx_audit_log_actor ON audit_log(actor_type, actor_id);
CREATE INDEX idx_audit_log_created_at ON audit_log(created_at);
CREATE INDEX idx_audit_log_action ON audit_log(action);
CREATE INDEX idx_game_statistics_type_key ON game_statistics(stat_type, stat_key);
CREATE INDEX idx_game_statistics_recorded_at ON game_statistics(recorded_at);
CREATE INDEX idx_performance_metrics_name ON performance_metrics(metric_name);
CREATE INDEX idx_performance_metrics_recorded_at ON performance_metrics(recorded_at);
-- Game tick indexes
CREATE INDEX idx_game_tick_log_tick_number ON game_tick_log(tick_number);
CREATE INDEX idx_game_tick_log_user_group ON game_tick_log(user_group);
CREATE INDEX idx_game_tick_log_status ON game_tick_log(status);
-- Event system indexes
CREATE INDEX idx_event_instances_event_type ON event_instances(event_type_id);
CREATE INDEX idx_event_instances_status ON event_instances(status);
CREATE INDEX idx_event_instances_start_time ON event_instances(start_time);
-- ===== ADD FOREIGN KEY CONSTRAINTS =====
-- Add foreign key constraints that couldn't be added during table creation
ALTER TABLE system_config ADD CONSTRAINT fk_system_config_admin FOREIGN KEY (updated_by) REFERENCES admin_users(id);
ALTER TABLE event_instances ADD CONSTRAINT fk_event_instances_admin FOREIGN KEY (created_by) REFERENCES admin_users(id);
-- ===== TRIGGERS FOR AUTOMATIC TIMESTAMPS =====
-- Function to automatically update updated_at timestamps
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply updated_at triggers to relevant tables
CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_game_tick_config_updated_at BEFORE UPDATE ON game_tick_config FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_event_instances_updated_at BEFORE UPDATE ON event_instances FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_plugins_updated_at BEFORE UPDATE ON plugins FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_admin_users_updated_at BEFORE UPDATE ON admin_users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_players_updated_at BEFORE UPDATE ON players FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_player_settings_updated_at BEFORE UPDATE ON player_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_colonies_updated_at BEFORE UPDATE ON colonies FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_colony_buildings_updated_at BEFORE UPDATE ON colony_buildings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_ship_designs_updated_at BEFORE UPDATE ON ship_designs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ===== INITIAL DATA =====
-- Insert initial system configuration
INSERT INTO system_config (config_key, config_value, config_type, description, is_public) VALUES
('game_tick_interval_ms', '60000', 'number', 'Game tick interval in milliseconds', false),
('max_user_groups', '10', 'number', 'Maximum number of user groups for tick processing', false),
('max_retry_attempts', '5', 'number', 'Maximum retry attempts for failed ticks', false),
('data_retention_days', '30', 'number', 'Default data retention period in days', false),
('max_colonies_per_player', '10', 'number', 'Maximum colonies a player can own', true),
('starting_resources', '{"scrap": 1000, "energy": 500, "data_cores": 0, "rare_elements": 0}', 'json', 'Starting resources for new players', false),
('websocket_ping_interval', '30000', 'number', 'WebSocket ping interval in milliseconds', false);
-- Insert resource types
INSERT INTO resource_types (name, description, category, is_tradeable) VALUES
('scrap', 'Basic salvaged materials from the ruins of civilization', 'basic', true),
('energy', 'Power cells and energy storage units', 'basic', true),
('data_cores', 'Advanced information storage containing pre-collapse knowledge', 'advanced', true),
('rare_elements', 'Exotic materials required for advanced technologies', 'rare', true);
-- Insert initial planet types
INSERT INTO planet_types (name, description, base_resources, resource_modifiers, max_population, rarity_weight) VALUES
('Terran', 'Earth-like worlds with balanced resources', '{"scrap": 500, "energy": 300}', '{"scrap": 1.0, "energy": 1.0, "data_cores": 1.0, "rare_elements": 1.0}', 10000, 100),
('Industrial', 'Former manufacturing worlds rich in salvageable materials', '{"scrap": 1000, "energy": 200}', '{"scrap": 1.5, "energy": 0.8, "data_cores": 1.2, "rare_elements": 0.9}', 8000, 80),
('Research', 'Academic worlds with data archives and laboratories', '{"scrap": 300, "energy": 400, "data_cores": 100}', '{"scrap": 0.7, "energy": 1.1, "data_cores": 2.0, "rare_elements": 1.3}', 6000, 60),
('Mining', 'Resource extraction worlds with rare element deposits', '{"scrap": 400, "energy": 250, "rare_elements": 20}', '{"scrap": 1.1, "energy": 0.9, "data_cores": 0.8, "rare_elements": 2.5}', 7000, 70),
('Fortress', 'Heavily fortified military installations', '{"scrap": 600, "energy": 500}', '{"scrap": 1.2, "energy": 1.3, "data_cores": 1.1, "rare_elements": 1.1}', 5000, 40),
('Derelict', 'Abandoned worlds with scattered ruins and mysteries', '{"scrap": 200, "energy": 100, "data_cores": 50, "rare_elements": 10}', '{"scrap": 0.8, "energy": 0.6, "data_cores": 1.5, "rare_elements": 1.8}', 3000, 30);
-- Insert initial building types
INSERT INTO building_types (name, description, category, max_level, base_cost, base_production, special_effects) VALUES
('Command Center', 'Central administration building that coordinates colony operations', 'infrastructure', 10, '{"scrap": 100, "energy": 50}', '{}', '{"colony_slots": 1}'),
('Salvage Yard', 'Processes scrap metal and salvageable materials', 'production', 10, '{"scrap": 50, "energy": 25}', '{"scrap": 10}', '{}'),
('Power Plant', 'Generates energy for colony operations', 'production', 10, '{"scrap": 75, "energy": 0}', '{"energy": 8}', '{}'),
('Research Lab', 'Conducts technological research and development', 'research', 10, '{"scrap": 100, "energy": 75, "data_cores": 10}', '{}', '{"research_speed": 0.1}'),
('Housing Complex', 'Provides living space for colony population', 'infrastructure', 15, '{"scrap": 60, "energy": 30}', '{}', '{"population_capacity": 100}'),
('Defense Grid', 'Automated defense systems protecting the colony', 'military', 10, '{"scrap": 150, "energy": 100, "rare_elements": 5}', '{}', '{"defense_rating": 50}'),
('Data Archive', 'Stores and processes information from data cores', 'research', 8, '{"scrap": 80, "energy": 60, "data_cores": 5}', '{"data_cores": 2}', '{"research_bonus": 0.05}'),
('Mining Facility', 'Extracts rare elements from planetary deposits', 'production', 12, '{"scrap": 200, "energy": 150, "data_cores": 20}', '{"rare_elements": 1}', '{}');
-- Insert initial game tick configuration
INSERT INTO game_tick_config (tick_interval_ms, user_groups_count, max_retry_attempts, bonus_tick_threshold) VALUES
(60000, 10, 5, 3);
-- Insert basic combat type
INSERT INTO combat_types (name, description, config) VALUES
('instant_resolution', 'Basic instant combat resolution with detailed logs', '{"calculate_experience": true, "detailed_logs": true}');
-- Insert initial event types
INSERT INTO event_types (name, description, trigger_type, config_schema) VALUES
('galaxy_crisis', 'Major galaxy-wide crisis events', 'admin', '{"duration_hours": {"type": "number", "min": 1, "max": 168}}'),
('discovery_event', 'Random discovery events triggered by exploration', 'player', '{"discovery_type": {"type": "string", "enum": ["artifact", "technology", "resource"]}}'),
('faction_war', 'Large-scale conflicts between factions', 'mixed', '{"participating_factions": {"type": "array", "items": {"type": "number"}}}');
-- Insert data retention policies
INSERT INTO data_retention_policies (table_name, retention_days, archive_before_delete) VALUES
('audit_log', 90, true),
('game_tick_log', 7, false),
('battles', 30, true),
('performance_metrics', 14, false),
('game_statistics', 365, true);
-- Insert initial galaxy sectors
INSERT INTO galaxy_sectors (name, coordinates, description, danger_level) VALUES
('Core Worlds', 'A1', 'The former heart of galactic civilization', 3),
('Industrial Belt', 'B2', 'Manufacturing and resource processing sector', 4),
('Frontier Region', 'C3', 'Outer rim territories with unexplored systems', 6),
('Research Zone', 'D4', 'Former academic and scientific installations', 2),
('Contested Space', 'E5', 'Battlegrounds of the great collapse', 8),
('Dead Zone', 'F6', 'Heavily damaged region with dangerous anomalies', 9);
-- Create initial admin user (password should be changed immediately)
INSERT INTO admin_users (username, email, password_hash, role, permissions) VALUES
('admin', 'admin@shatteredvoid.game', '$2b$10$example.hash.change.immediately', 'administrator', '["all"]');
-- Insert initial plugin for basic combat
INSERT INTO plugins (name, version, description, plugin_type, is_active, config, hooks) VALUES
('basic_combat', '1.0.0', 'Basic instant combat resolution system', 'combat', true, '{"damage_variance": 0.1, "experience_gain": 1.0}', '["pre_combat", "post_combat", "damage_calculation"]');

181
idea.txt Normal file
View file

@ -0,0 +1,181 @@
**Project Name:** Shattered Void (working title)
**Genre:** Text-based Post-Collapse Sci-Fi MMO Strategy Game (Inspired by OGame)
---
## Overview
**Shattered Void** is a browser-based multiplayer strategy game set in a decaying galaxy after the collapse of a vast interstellar civilization. Players command remnants of once-great empires, establishing colonies, rebuilding technology, constructing modular fleets, and engaging in diplomacy, espionage, and war. The game is played in real-time, with asynchronous elements, a text-based star map, rich lore, and a dynamic event system.
---
## Core Gameplay Loops
### 1. **Base Building and Resource Management**
* Players start with a ruined colony on a planet.
* Resources include: Scrap, Energy, Data Cores, Rare Elements.
* Players restore old infrastructure and build new facilities.
* Research technology to unlock systems, fleet modules, and faction upgrades.
### 2. **Exploration and Expansion**
* Text-based starmap with coordinates (e.g., Sectors: A3-91).
* Explore ruins, derelict stations, dead stars.
* Colonize new worlds, salvage remnants, uncover pre-collapse secrets.
* Galaxy map reveals gradually through exploration or scouting.
### 3. **Modular Ship Construction**
* Ships are built from modular components:
* **Hull Types:** Frigate, Cruiser, Dreadnought, Carrier, Scout.
* **Modules:** Weapons, Engines, Shields, Utility, AI Cores.
* Players unlock more parts through research, salvage, and events.
* Players can save and iterate on custom ship designs.
### 4. **Combat System**
* Fleet vs Fleet, Fleet vs Defenses.
* Simulated in background; outcomes are reported in rich-text combat logs.
* Tactical choices (loadouts, formations, pre-battle stance) influence results.
### 5. **Dynamic Galaxy Event System**
* Events affect entire sectors or the whole galaxy:
* Pirate uprisings
* Alien incursions
* Black hole migrations
* Data Plague (AI virus)
* Events have persistent consequences, influence lore and gameplay.
* Players can influence or be recruited into resolving/triggering events.
### 6. **Faction and Diplomacy System**
* Players can form factions (or join ancient ones) with shared bonuses.
* Factions can vote on technology paths, coordinate expansion, or wage war.
* Diplomatic mechanics include:
* Treaties, Trade Routes
* Espionage and Sabotage
* Influence mechanics (soft power over neutral systems)
### 7. **Legacy/Prestige System**
* If a player is conquered or resets, they can:
* Leave behind artifacts or derelict fleets
* Transfer a fraction of resources/tech
* Unlock new factions or perks for future runs
---
## Text-Based Star Map System
* The galaxy is divided into Sectors and Systems (e.g., A3-91-X).
* Each system has flavor-rich descriptions:
* Ex: "System A3-91-X: A husk of a once-thriving mining colony now orbits a cracked gas giant, covered in radioactive mist."
* Systems may contain:
* Colonizable planets
* Event triggers
* Ruins with lore and salvage
* Pirate bases, rogue AI nests, alien remains
* Exploration reveals these gradually, encouraging scouting.
---
## Technical Systems Breakdown
### Backend
* Language: Python (FastAPI or Django), Node.js (Express), or Go.
* Real-time tick system for production, movement, and research
* PostgreSQL (main DB), Redis (caching/session management)
* JWT-based user authentication
* Modular ship builder engine
* Combat simulator (cron-based or real-time triggered)
* REST API for frontend communication
* Asynchronous task queue (Celery, Bull, or Go Routines)
* Admin CMS for content, events, and balancing
### Frontend / UI
* Framework: React or Svelte (SPA)
* Clean UI with text emphasis, stylized with CSS/Tailwind or custom themes
* Star map navigation via coordinates (e.g., input sector to load system)
* Resource HUD, alerts/notifications bar, event tracker, logs
* Modular ship designer drag-and-drop panel
* Side panel menus for base, research, fleet, diplomacy
### Website Features
* Landing page with lore, signup/login
* Secure authentication system (email-based, with verification)
* Player dashboard (active colonies, alerts, missions)
* In-game messaging and faction chat
* Account settings and faction selection
### Game Systems and Database Schema (Core Tables)
* **Users:** id, email, password, created\_at, preferences
* **Colonies:** id, user\_id, name, planet\_type, coordinates, population, morale
* **Buildings:** id, colony\_id, type, level, construction\_start, end\_time
* **Resources:** colony\_id, scrap, energy, data\_cores, rare\_elements
* **Fleets:** id, user\_id, location, status, composition, destination, eta
* **ShipModules:** id, type, stats (JSON), rarity, source (research/salvage)
* **Ships:** id, fleet\_id, design\_id, hp, module\_list
* **TechTree:** id, user\_id, tech\_type, level, prerequisites, research\_time
* **Events:** id, type, description, start\_time, end\_time, effects (JSON)
* **Factions:** id, name, banner\_url, traits (JSON), members, influence
### Building Types (Universal Structure)
* **Scrap Docks:** Generates Scrap over time
* **Energy Grid:** Produces power to fuel systems
* **Data Silos:** Increases Data Core generation
* **Element Extractors:** Mine Rare Elements from planetary crust
* **Habitat Blocks:** Increases population and morale
* **Barracks:** Trains and maintains ground units (used in invasions)
* **Hangar Bays:** Construct and house space fleets
* **Tech Labs:** Unlock and speed up research
### Game Loop Logic
* Every X seconds (tick interval):
* Buildings produce resources based on level
* Fleets move along calculated trajectories
* Events progress or trigger
* Research and building queues complete
* Notifications generated and pushed to user
### Admin Tools
* Web panel for lore editing and content entries
* Event creator and scheduler
* Player search, moderation, data adjustment
* Server tick controller
---
## Example Starting Scenario
"You awaken within the ruins of Outpost 7-X, a crumbling orbital station barely holding atmosphere. With a half-functional fabrication bay and trace energy readings from the planet below, you must reclaim what you can from the dead machines and begin again. Communications are silent. The stars around you are cold."
---
## Future Ideas (Optional)
* Wormhole travel and alternate dimensions
* Player-run trade hubs
* Organic factions (e.g., evolved alien hives)
* AI rebellion threat mechanic
* War heroes and persistent admiral characters
---
This document serves as a foundational blueprint for creating a post-collapse, text-based MMO inspired by OGame but with modern storytelling, system design, and player agency. It also includes backend architecture, website structure, database models, and scalable design patterns for development.

74
knexfile.js Normal file
View file

@ -0,0 +1,74 @@
require('dotenv').config({ path: `.env.${process.env.NODE_ENV || 'development'}` });
const config = {
development: {
client: 'postgresql',
connection: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'shattered_void_dev',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
},
pool: {
min: parseInt(process.env.DB_POOL_MIN) || 2,
max: parseInt(process.env.DB_POOL_MAX) || 10,
acquireTimeoutMillis: parseInt(process.env.DB_TIMEOUT) || 60000,
},
migrations: {
directory: './src/database/migrations',
tableName: 'knex_migrations',
},
seeds: {
directory: './src/database/seeds',
},
},
test: {
client: 'postgresql',
connection: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'shattered_void_test',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
},
pool: {
min: 1,
max: 5,
},
migrations: {
directory: './src/database/migrations',
tableName: 'knex_migrations',
},
seeds: {
directory: './src/database/seeds',
},
},
production: {
client: 'postgresql',
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
},
pool: {
min: parseInt(process.env.DB_POOL_MIN) || 5,
max: parseInt(process.env.DB_POOL_MAX) || 20,
acquireTimeoutMillis: parseInt(process.env.DB_TIMEOUT) || 60000,
},
migrations: {
directory: './src/database/migrations',
tableName: 'knex_migrations',
},
seeds: {
directory: './src/database/seeds',
},
},
};
module.exports = config;

10390
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

106
package.json Normal file
View file

@ -0,0 +1,106 @@
{
"name": "shattered-void-mmo",
"version": "0.1.0",
"description": "Shattered Void - A post-collapse galaxy MMO strategy game",
"main": "src/server.js",
"scripts": {
"dev": "nodemon --inspect=0.0.0.0:9229 src/server.js",
"start": "node src/server.js",
"test": "jest --verbose --coverage",
"test:watch": "jest --watch --verbose",
"test:integration": "jest --testPathPattern=integration --runInBand",
"test:e2e": "jest --testPathPattern=e2e --runInBand",
"lint": "eslint src/ --ext .js --fix",
"lint:check": "eslint src/ --ext .js",
"db:migrate": "knex migrate:latest",
"db:rollback": "knex migrate:rollback",
"db:seed": "knex seed:run",
"db:reset": "knex migrate:rollback:all && knex migrate:latest && knex seed:run",
"db:setup": "createdb shattered_void_dev && npm run db:migrate && npm run db:seed",
"setup": "node scripts/setup.js",
"docker:build": "docker build -t shattered-void .",
"docker:run": "docker-compose up -d",
"docker:dev": "docker-compose -f docker-compose.dev.yml up -d",
"logs": "tail -f logs/combined.log",
"logs:error": "tail -f logs/error.log",
"logs:audit": "tail -f logs/audit.log"
},
"dependencies": {
"bcrypt": "^5.1.1",
"compression": "^1.7.4",
"connect-redis": "^7.1.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-session": "^1.17.3",
"express-validator": "^7.0.1",
"express-ws": "^5.0.2",
"helmet": "^7.1.0",
"joi": "^17.11.1",
"jsonwebtoken": "^9.0.2",
"knex": "^3.0.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.7",
"pg": "^8.11.3",
"rate-limit-redis": "^4.2.1",
"redis": "^4.6.11",
"socket.io": "^4.8.1",
"uuid": "^9.0.1",
"validator": "^13.15.15",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"ws": "^8.14.2"
},
"devDependencies": {
"@types/jest": "^29.5.8",
"eslint": "^8.56.0",
"eslint-config-node": "^4.1.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"supertest": "^6.3.3"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
},
"keywords": [
"mmo",
"strategy",
"game",
"nodejs",
"postgresql",
"redis",
"websocket"
],
"author": "Shattered Void Development Team",
"license": "UNLICENSED",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/your-org/shattered-void-mmo.git"
},
"jest": {
"testEnvironment": "node",
"collectCoverageFrom": [
"src/**/*.js",
"!src/server.js",
"!src/database/migrations/**",
"!src/database/seeds/**"
],
"coverageDirectory": "coverage",
"coverageReporters": [
"text",
"lcov",
"html"
],
"testMatch": [
"**/tests/**/*.test.js"
]
}
}

172
scripts/setup.sh Executable file
View file

@ -0,0 +1,172 @@
#!/bin/bash
# Shattered Void MMO - Setup Script
# Automates the initial setup process for development environment
set -e # Exit on any error
echo "🌌 Shattered Void MMO - Development Setup"
echo "========================================"
# Check prerequisites
echo "📋 Checking prerequisites..."
if ! command -v node &> /dev/null; then
echo "❌ Node.js is not installed. Please install Node.js 16+ and try again."
exit 1
fi
if ! command -v npm &> /dev/null; then
echo "❌ npm is not installed. Please install npm 8+ and try again."
exit 1
fi
if ! command -v psql &> /dev/null; then
echo "❌ PostgreSQL client is not installed. Please install PostgreSQL and try again."
exit 1
fi
if ! command -v redis-cli &> /dev/null; then
echo "⚠️ Redis client not found. Make sure Redis server is running."
fi
echo "✅ Prerequisites check passed"
# Check Node.js version
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 16 ]; then
echo "❌ Node.js version $NODE_VERSION is not supported. Please upgrade to Node.js 16+"
exit 1
fi
echo "✅ Node.js version check passed"
# Install dependencies
echo ""
echo "📦 Installing npm dependencies..."
npm install
echo "✅ Dependencies installed"
# Check for .env file
echo ""
echo "⚙️ Checking environment configuration..."
if [ ! -f ".env" ]; then
echo "📝 Creating .env file from template..."
cp .env.example .env
echo "⚠️ Please edit .env file with your database credentials and configuration"
echo " Database settings, JWT secret, and other configurations need to be updated"
else
echo "✅ .env file already exists"
fi
# Check database connection
echo ""
echo "🗄️ Checking database setup..."
# Load environment variables
if [ -f ".env" ]; then
export $(grep -v '^#' .env | xargs)
fi
# Set defaults if not in .env
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-shattered_void_dev}
DB_USER=${DB_USER:-postgres}
echo "Attempting to connect to database: $DB_NAME@$DB_HOST:$DB_PORT"
# Test database connection
if ! pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" 2>/dev/null; then
echo "❌ Cannot connect to PostgreSQL database"
echo " Please ensure PostgreSQL is running and the database exists"
echo " Run these commands to create the database:"
echo " sudo -u postgres psql"
echo " CREATE DATABASE $DB_NAME;"
echo " CREATE USER $DB_USER WITH PASSWORD 'your_password';"
echo " GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
echo " \\q"
exit 1
fi
echo "✅ Database connection successful"
# Apply database schema
echo ""
echo "🏗️ Setting up database schema..."
if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f database-schema.sql > /dev/null 2>&1; then
echo "✅ Database schema applied successfully"
else
echo "❌ Failed to apply database schema"
echo " Please check your database credentials and permissions"
exit 1
fi
# Run migrations
echo ""
echo "🔄 Running database migrations..."
if npm run db:migrate > /dev/null 2>&1; then
echo "✅ Database migrations completed"
else
echo "⚠️ Database migrations failed - this may be normal if already applied"
fi
# Run seeds
echo ""
echo "🌱 Seeding initial data..."
if npm run db:seed > /dev/null 2>&1; then
echo "✅ Initial data seeded successfully"
else
echo "❌ Failed to seed initial data"
echo " You may need to run: npm run db:seed manually"
fi
# Test Redis connection
echo ""
echo "📡 Testing Redis connection..."
REDIS_HOST=${REDIS_HOST:-localhost}
REDIS_PORT=${REDIS_PORT:-6379}
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ping > /dev/null 2>&1; then
echo "✅ Redis connection successful"
else
echo "⚠️ Redis connection failed"
echo " Redis is optional but recommended for production"
echo " Install and start Redis server for optimal performance"
fi
# Create logs directory
echo ""
echo "📝 Setting up logging..."
mkdir -p logs
touch logs/combined.log
touch logs/error.log
echo "✅ Log directory created"
# Run linting
echo ""
echo "🔍 Running code quality checks..."
if npm run lint:check > /dev/null 2>&1; then
echo "✅ Code quality checks passed"
else
echo "⚠️ Code quality issues found - run 'npm run lint' to fix"
fi
# Final setup complete
echo ""
echo "🎉 Setup completed successfully!"
echo ""
echo "Next steps:"
echo "1. Review and update the .env file with your configuration"
echo "2. Start the development server: npm run dev"
echo "3. Visit http://localhost:3000 to verify the server is running"
echo ""
echo "Development commands:"
echo " npm run dev - Start development server with hot reload"
echo " npm test - Run test suite"
echo " npm run lint - Check and fix code style"
echo ""
echo "For more information, see README.md"
echo ""
echo "🌌 Welcome to Shattered Void MMO development!"

View file

@ -0,0 +1,469 @@
# Shattered Void: The Ultimate Post-Collapse MMO Strategy Game
## A Comprehensive Game Development Proposal
---
## 🌌 Executive Summary
**Shattered Void** is a revolutionary browser-based MMO strategy game that combines the depth of classic space strategy with modern storytelling and social mechanics. Set in a decaying galaxy after civilizational collapse, players rebuild empires from ruins while engaging in diplomacy, exploration, and epic galaxy-wide events.
**Target Market:** 25-45 year old strategy game enthusiasts seeking deep, persistent gameplay that respects their time
**Platform:** Cross-platform web application (PC/Mobile/Tablet)
**Development Timeline:** 18-24 months to full launch
**Revenue Model:** Premium subscriptions with cosmetic monetization
---
## 🎮 Game Overview
### Core Vision
Transform the traditional space strategy genre by focusing on **narrative-driven events**, **meaningful player choices**, and **collaborative galaxy-shaping experiences**. Every action contributes to a persistent, evolving universe where player stories become galactic legend.
### Unique Selling Points
- **Living Galaxy**: Dynamic events permanently alter the game world
- **True Cross-Platform**: Seamless experience across all devices
- **Deep Customization**: Modular ship building and colony specialization
- **Respectful Gameplay**: Strategic depth without predatory mechanics
- **Community-Driven**: Player actions shape ongoing narrative
---
## 🚀 Core Gameplay Systems
### 1. Colony Management & Resource Economy
Players start with a single ruined outpost and must rebuild civilization through strategic resource management:
**Resource Types:**
- **Scrap**: Basic construction material salvaged from ruins
- **Energy**: Powers all colony operations and advanced systems
- **Data Cores**: Pre-collapse knowledge used for research
- **Rare Elements**: Exotic materials for advanced technology
**Building Progression:**
- **Tier 1**: Basic Infrastructure (Scrap Docks, Energy Grids, Habitats)
- **Tier 2**: Specialized Production (Element Extractors, Tech Labs)
- **Tier 3**: Military & Defense (Hangar Bays, Shield Generators)
- **Tier 4**: Advanced Systems (Quantum Forges, Void Gates)
- **Tier 5**: Endgame Wonders (Genesis Chambers, AI Cores)
### 2. Modular Fleet Construction
Revolutionary ship building system where players design custom vessels:
**Hull Types:** Scout, Frigate, Cruiser, Dreadnought, Carrier, Titan
**Module Categories:** Weapons, Engines, Shields, Utility, AI Cores
**Design Philosophy:** No "best" ship - only ships optimized for specific roles
### 3. Text-Based Exploration
Rich, descriptive galaxy exploration emphasizing imagination over graphics:
- **Coordinate System**: Navigate via sector coordinates (A3-91-X)
- **Procedural Discoveries**: Ruins, derelicts, and mysteries await
- **Persistent World**: Player discoveries permanently change the galaxy
- **Collaborative Mapping**: Community efforts reveal galactic secrets
### 4. Dynamic Galaxy Events
Living world system where admin-driven events create shared experiences:
**Event Categories:**
- **Crisis Events**: Galaxy-threatening challenges requiring cooperation
- **Discovery Events**: Uncover ancient secrets and lost technology
- **Conflict Events**: Large-scale wars and territorial disputes
- **Celebration Events**: Community festivals and special occasions
---
## 🏗️ Technical Architecture
### Backend Infrastructure
- **Language**: Node.js with Express framework
- **Database**: PostgreSQL (primary) + Redis (caching/sessions)
- **Real-Time**: WebSocket connections for live updates
- **Authentication**: JWT-based with email verification
- **Game Loop**: Asynchronous tick system for continuous world simulation
### Frontend Experience
- **Framework**: React with responsive design
- **UI Philosophy**: Clean, text-focused interface with strategic depth
- **Mobile Optimization**: Touch-friendly controls and condensed layouts
- **PWA Features**: Installable app experience across all platforms
### Data-Driven Design
All game content stored in configurable databases allowing rapid balancing and content updates without code changes.
---
## 👑 Administrative Excellence
### Ultra-Secure Admin Panel
- **Multi-Factor Authentication**: Hardware tokens + IP restrictions
- **Hidden Access**: Obfuscated routes with rotating security keys
- **Complete Audit Trail**: Every admin action logged and monitored
### Comprehensive Management Tools
- **User Investigation Suite**: Deep player behavior analysis
- **Real-Time Logging**: Live system monitoring and debugging
- **Game Content Editor**: Visual tools for modifying all game elements
- **Balance Testing**: Simulation tools for testing changes safely
### Live Event Management
- **Visual Event Editor**: Drag-and-drop event creation tools
- **Real-Time Control**: Monitor and adjust events as they happen
- **Community Impact**: Track player engagement and satisfaction
---
## 💰 Monetization Strategy
### Ethical Revenue Model
- **Premium Subscriptions**: $9.99/month for enhanced features
- **Cosmetic Items**: Ship skins, colony themes, player avatars
- **Quality of Life**: Extra build queues, expanded fleet commands
- **NO Pay-to-Win**: All gameplay advantages earned through play
### Subscription Benefits
- **Expanded Queues**: Multiple simultaneous research/building projects
- **Advanced Analytics**: Detailed statistics and optimization tools
- **Priority Support**: Faster customer service response
- **Exclusive Cosmetics**: Subscriber-only customization options
---
## 📊 Market Analysis
### Target Demographics
- **Primary**: 25-45 year old strategy enthusiasts with disposable income
- **Secondary**: Former MMO players seeking meaningful, respectful gameplay
- **Tertiary**: Mobile strategy players wanting deeper experiences
### Competitive Advantages
- **Respect Player Time**: No forced waiting or aggressive monetization
- **Cross-Platform Native**: True seamless experience across devices
- **Living World**: Events create unique, non-repeatable experiences
- **Community Focus**: Player actions have permanent world impact
### Revenue Projections
- **Year 1**: 10,000 active players, 25% subscription rate → $300K ARR
- **Year 2**: 50,000 active players, 30% subscription rate → $1.8M ARR
- **Year 3**: 100,000 active players, 35% subscription rate → $4.2M ARR
---
## 🗓️ Development Roadmap
### Phase 1: Foundation (Months 1-6)
**Core Systems Development**
- Database architecture and user authentication
- Basic colony management and resource systems
- Simple fleet construction and movement
- Text-based galaxy exploration framework
**Milestone Goal**: Single-player colony management demo
### Phase 2: Multiplayer (Months 7-12)
**Social and Combat Systems**
- Multi-player infrastructure and real-time updates
- Fleet combat simulation and battle logs
- Basic diplomacy and faction systems
- Simple galaxy events framework
**Milestone Goal**: Closed beta with 100 invited players
### Phase 3: Polish (Months 13-18)
**Advanced Features and Balance**
- Complete admin panel and content management
- Advanced ship building and customization
- Complex galaxy events and narrative system
- Mobile optimization and PWA features
**Milestone Goal**: Open beta with 1,000+ players
### Phase 4: Launch (Months 19-24)
**Production Ready**
- Performance optimization and scaling
- Comprehensive tutorial and new player experience
- Marketing campaign and community building
- Post-launch content pipeline establishment
**Milestone Goal**: Full public launch
---
## ✅ Comprehensive Development Todo List
### 🏗️ **FOUNDATION PHASE (High Priority)**
#### **Database & Backend Infrastructure**
- [ ] Design normalized database schema with full audit logging
- [ ] Implement user authentication system with JWT and email verification
- [ ] Create modular service architecture (colony, fleet, research, combat services)
- [ ] Set up database migrations and version control system
- [ ] Implement comprehensive event sourcing for all user actions
- [ ] Create real-time tick system for game world simulation
- [ ] Set up Redis caching layer for performance optimization
- [ ] Implement WebSocket infrastructure for live updates
#### **Core Game Systems**
- [ ] Build colony management system with building construction queues
- [ ] Create resource production and consumption mechanics
- [ ] Implement research system with technology trees
- [ ] Design modular ship construction system with component validation
- [ ] Create fleet movement and logistics systems
- [ ] Build text-based galaxy exploration with coordinate navigation
- [ ] Implement basic combat simulation engine with detailed logging
#### **Frontend Development**
- [ ] Set up React application with responsive design framework
- [ ] Create main game dashboard with resource monitoring
- [ ] Build colony management interface with building placement
- [ ] Design ship construction interface with drag-and-drop modules
- [ ] Implement galaxy map navigation with coordinate input
- [ ] Create research interface with technology tree visualization
- [ ] Build fleet management and movement interfaces
### 🌐 **MULTIPLAYER PHASE (High Priority)**
#### **Social Systems**
- [ ] Implement faction creation and management system
- [ ] Create player-to-player messaging and communication
- [ ] Build diplomacy system with treaties and trade agreements
- [ ] Design reputation and influence mechanics
- [ ] Create faction voting and governance systems
- [ ] Implement alliance and enemy relationship tracking
#### **Advanced Combat**
- [ ] Build fleet vs fleet combat with formation tactics
- [ ] Create siege mechanics for colony invasion
- [ ] Implement defensive structures and shield systems
- [ ] Design combat replay system with step-by-step analysis
- [ ] Create battle result notification and logging system
#### **Galaxy Events Foundation**
- [ ] Design modular event system with trigger conditions
- [ ] Create event participation tracking and scoring
- [ ] Implement basic event reward distribution system
- [ ] Build event notification and communication system
- [ ] Create event history and legacy tracking
### 🔧 **ADMINISTRATIVE SYSTEMS (High Priority)**
#### **Ultra-Secure Admin Panel**
- [ ] Build multi-factor authentication system for admin access
- [ ] Create IP whitelist and access control mechanisms
- [ ] Implement session management with automatic timeout
- [ ] Design obfuscated admin routes with rotating access keys
- [ ] Create admin permission system with role-based access
#### **User Management & Investigation**
- [ ] Build comprehensive user lookup with complete action history
- [ ] Create IP address tracking and geolocation analysis
- [ ] Implement device fingerprinting and session correlation
- [ ] Design behavior pattern analysis and anomaly detection
- [ ] Create account linking detection and investigation tools
- [ ] Build cheat/exploit detection and alerting system
#### **System Monitoring & Debugging**
- [ ] Create real-time logging dashboard with advanced filtering
- [ ] Implement live system performance monitoring
- [ ] Build database query analysis and optimization tools
- [ ] Create WebSocket connection monitoring and debugging
- [ ] Design game state inspection and modification tools
- [ ] Implement error aggregation and alerting system
#### **Game Content Management**
- [ ] Build visual building editor with stat modification
- [ ] Create ship component designer with balance validation
- [ ] Design technology tree editor with prerequisite management
- [ ] Implement galaxy content editor for sectors and planets
- [ ] Create event template editor with visual workflow builder
- [ ] Build game balance testing and simulation tools
### 🎭 **ADVANCED FEATURES (Medium Priority)**
#### **Galaxy Events System**
- [ ] Create visual event editor with drag-and-drop workflows
- [ ] Implement event phase progression and narrative branching
- [ ] Build dynamic reward system with conditional distribution
- [ ] Create real-time event monitoring and control dashboard
- [ ] Design event consequence system with permanent world changes
- [ ] Implement cross-event continuity and story progression
#### **Advanced Gameplay**
- [ ] Create prestige/legacy system for veteran players
- [ ] Implement advanced diplomacy with espionage and sabotage
- [ ] Build trade route optimization and economic modeling
- [ ] Create advanced fleet formations and tactical commands
- [ ] Design mega-projects and endgame content systems
- [ ] Implement player-driven content creation tools
#### **Mobile & Accessibility**
- [ ] Optimize UI for mobile touch interfaces
- [ ] Implement swipe gestures and mobile-specific controls
- [ ] Create progressive web app (PWA) functionality
- [ ] Design offline mode for basic game monitoring
- [ ] Implement accessibility features for disabled players
- [ ] Create simplified mobile interface options
### 📊 **ANALYTICS & OPTIMIZATION (Medium Priority)**
#### **Data Collection & Analysis**
- [ ] Implement comprehensive player behavior tracking
- [ ] Create game balance metrics and monitoring dashboards
- [ ] Build player retention analysis and prediction models
- [ ] Design A/B testing framework for feature rollouts
- [ ] Create revenue tracking and subscription analytics
- [ ] Implement performance profiling and optimization tools
#### **Community & Support**
- [ ] Build in-game help system and tutorial framework
- [ ] Create community forums integration
- [ ] Design player feedback collection and analysis system
- [ ] Implement customer support ticketing system
- [ ] Create automated FAQ and help documentation
- [ ] Build community moderation tools and systems
### 🚀 **LAUNCH PREPARATION (Lower Priority)**
#### **Performance & Scaling**
- [ ] Implement database sharding and horizontal scaling
- [ ] Create load balancing and traffic distribution
- [ ] Design disaster recovery and backup systems
- [ ] Implement CDN integration for global performance
- [ ] Create automated deployment and rollback systems
- [ ] Build comprehensive monitoring and alerting infrastructure
#### **Marketing & Community**
- [ ] Create game trailer and promotional materials
- [ ] Build landing page and marketing website
- [ ] Design social media integration and sharing features
- [ ] Implement referral system and community growth tools
- [ ] Create press kit and media relations materials
- [ ] Build influencer and content creator partnership program
---
## 🧪 Testing Strategy & Quality Assurance
### **Alpha Testing Goals (Months 4-8)**
**Objective**: Validate core gameplay mechanics and technical stability
#### **Technical Testing**
- [ ] **Load Testing**: Simulate 1,000 concurrent users across all systems
- [ ] **Database Performance**: Ensure sub-100ms response times for all queries
- [ ] **Security Penetration**: Third-party security audit of authentication and admin systems
- [ ] **Cross-Platform Compatibility**: Test on 10+ device/browser combinations
- [ ] **Real-Time Sync**: Validate WebSocket performance under various network conditions
#### **Gameplay Testing**
- [ ] **New Player Experience**: Track tutorial completion rates (target: >80%)
- [ ] **Resource Balance**: Monitor economy for inflation/deflation patterns
- [ ] **Fleet Combat**: Validate combat simulation accuracy and fairness
- [ ] **Building Progression**: Ensure logical upgrade paths and balance
- [ ] **Galaxy Exploration**: Test coordinate system and discovery mechanics
#### **Success Metrics**
- **Player Retention**: 70% day-1, 40% day-7, 20% day-30
- **Technical Stability**: <1% crash rate, <5% bug reports per session
- **Performance**: Page load <2 seconds, action response <500ms
- **Player Satisfaction**: >4.0/5.0 average rating in feedback surveys
### **Beta Testing Goals (Months 9-15)**
**Objective**: Stress-test multiplayer systems and refine game balance
#### **Multiplayer Systems Testing**
- [ ] **Faction Warfare**: Organize large-scale conflicts with 500+ participants
- [ ] **Diplomatic Complexity**: Test alliance chains and treaty negotiations
- [ ] **Galaxy Events**: Run major events with server-wide participation
- [ ] **Admin Tools**: Validate all administrative systems under real conditions
- [ ] **Community Features**: Test messaging, forums, and social systems
#### **Balance & Progression Testing**
- [ ] **Economic Modeling**: Simulate 6-month player progression patterns
- [ ] **Technology Trees**: Validate research paths and unlock sequences
- [ ] **Ship Meta Evolution**: Monitor fleet composition trends and counter-strategies
- [ ] **Colony Specialization**: Test various building strategies and optimizations
- [ ] **Event Impact**: Measure event participation and satisfaction rates
#### **Success Metrics**
- **Concurrent Users**: Stable performance with 2,000+ simultaneous players
- **Player Engagement**: Average session 45+ minutes, 3+ sessions per week
- **Community Health**: <5% toxic behavior reports, active faction participation
- **Revenue Validation**: 25%+ subscription conversion rate among engaged players
### **Launch Readiness Testing (Months 16-18)**
**Objective**: Ensure production-ready stability and customer satisfaction
#### **Production Environment Testing**
- [ ] **Scaling Validation**: Test auto-scaling under traffic spikes
- [ ] **Disaster Recovery**: Validate backup systems and emergency procedures
- [ ] **Payment Processing**: Test all subscription and purchase flows
- [ ] **Customer Support**: Validate help desk and issue resolution processes
- [ ] **Content Pipeline**: Test rapid deployment of events and balance changes
#### **User Experience Polish**
- [ ] **Tutorial Optimization**: Achieve >85% completion rate for new players
- [ ] **Mobile Experience**: Ensure feature parity and usability across devices
- [ ] **Accessibility Compliance**: Meet WCAG 2.1 AA standards
- [ ] **Performance Optimization**: Achieve <1 second load times globally
- [ ] **Localization Testing**: Validate UI and content in target languages
#### **Launch Success Metrics**
- **Technical Performance**: 99.9% uptime, <100ms average response time
- **User Acquisition**: 10,000+ registrations in first month
- **Player Retention**: Maintain alpha/beta retention targets at scale
- **Revenue Achievement**: Hit $50K ARR within 90 days of launch
- **Community Growth**: Active forums, social media engagement, content creation
---
## 💡 Competitive Advantages
### **Innovation Areas**
- **Narrative Integration**: Events create permanent galaxy changes
- **Cross-Platform Excellence**: True seamless experience across all devices
- **Administrative Transparency**: Players can see how game evolves
- **Community Collaboration**: Player actions shape shared experiences
- **Respectful Monetization**: Premium features enhance rather than gate content
### **Technical Excellence**
- **Data-Driven Design**: Rapid content updates without code deployment
- **Comprehensive Analytics**: Deep insights into player behavior and game balance
- **Security Focus**: Bank-level protection for user data and game integrity
- **Scalable Architecture**: Built to handle explosive growth from day one
---
## 🎯 Success Metrics & KPIs
### **Player Metrics**
- **Daily Active Users (DAU)**: Target 10,000+ by end of Year 1
- **Monthly Active Users (MAU)**: Target 50,000+ by end of Year 1
- **Player Retention**: Day-1: 70%, Day-7: 40%, Day-30: 20%
- **Session Length**: Average 45+ minutes for engaged players
- **Player Lifetime Value**: $120+ for subscription players
### **Business Metrics**
- **Subscription Conversion**: 25%+ of active players
- **Monthly Recurring Revenue**: $100K+ by month 12
- **Customer Acquisition Cost**: <$15 per registered player
- **Churn Rate**: <5% monthly for subscription players
- **Net Promoter Score**: >50 for overall player satisfaction
### **Technical Metrics**
- **System Uptime**: 99.9%+ availability
- **Response Time**: <500ms for all user actions
- **Bug Report Rate**: <1% of player sessions
- **Security Incidents**: Zero successful breaches or data compromises
---
## 🌟 Conclusion
**Shattered Void** represents the evolution of strategy gaming - combining the depth players crave with the respect for their time they deserve. By focusing on meaningful choices, collaborative experiences, and ongoing narrative evolution, we create not just a game, but a living universe where every player's actions matter.
Our comprehensive development plan, rigorous testing strategy, and ethical monetization approach position us to capture significant market share in the growing strategy MMO space. With the right team and funding, **Shattered Void** will become the definitive post-collapse strategy experience.
**The galaxy awaits. Will you help rebuild it?**
---
*This document represents a comprehensive game development proposal. Implementation timelines and features may be adjusted based on team size, funding, and market feedback.*

119
src/app.js Normal file
View file

@ -0,0 +1,119 @@
/**
* Shattered Void MMO - Express Application Configuration
* Sets up Express app with all middleware, routes, and error handling
*/
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const { v4: uuidv4 } = require('uuid');
// Import utilities and configuration
const logger = require('./utils/logger');
// Import middleware
const corsMiddleware = require('./middleware/cors.middleware');
const { rateLimiters } = require('./middleware/rateLimit.middleware');
const { requestLogger } = require('./middleware/logging.middleware');
const { errorHandler } = require('./middleware/error.middleware');
// Import routes
const routes = require('./routes');
/**
* Create and configure Express application
* @returns {Object} Configured Express app
*/
function createApp() {
const app = express();
const NODE_ENV = process.env.NODE_ENV || 'development';
// Add correlation ID to all requests for tracing
app.use((req, res, next) => {
req.correlationId = uuidv4();
res.set('X-Correlation-ID', req.correlationId);
next();
});
// Security middleware
app.use(helmet({
contentSecurityPolicy: NODE_ENV === 'production' ? undefined : false,
crossOriginEmbedderPolicy: false, // Allow WebSocket connections
}));
// CORS middleware
app.use(corsMiddleware);
// Compression middleware
app.use(compression());
// Body parsing middleware
app.use(express.json({
limit: process.env.REQUEST_SIZE_LIMIT || '10mb',
verify: (req, res, buf) => {
// Store raw body for webhook verification if needed
req.rawBody = buf;
}
}));
app.use(express.urlencoded({
extended: true,
limit: process.env.REQUEST_SIZE_LIMIT || '10mb'
}));
// Cookie parsing middleware
app.use(cookieParser());
// Request logging middleware
app.use(requestLogger);
// Rate limiting middleware
app.use(rateLimiters.global);
// Health check endpoint (before other routes)
app.get('/health', (req, res) => {
const healthData = {
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '0.1.0',
environment: NODE_ENV,
uptime: process.uptime(),
memory: {
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
rss: Math.round(process.memoryUsage().rss / 1024 / 1024)
}
};
res.status(200).json(healthData);
});
// API routes
app.use('/', routes);
// 404 handler for unmatched routes
app.use('*', (req, res) => {
logger.warn('Route not found', {
correlationId: req.correlationId,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent')
});
res.status(404).json({
error: 'Not Found',
message: 'The requested resource was not found',
path: req.originalUrl,
timestamp: new Date().toISOString(),
correlationId: req.correlationId
});
});
// Global error handler (must be last)
app.use(errorHandler);
return app;
}
module.exports = createApp;

249
src/config/redis.js Normal file
View file

@ -0,0 +1,249 @@
/**
* Redis Configuration and Connection Management
* Handles Redis client initialization, connection management, and error handling
*/
const redis = require('redis');
const logger = require('../utils/logger');
// Configuration
const REDIS_CONFIG = {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
db: parseInt(process.env.REDIS_DB) || 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
lazyConnect: true,
connectTimeout: 10000,
commandTimeout: 5000,
};
let client = null;
let isConnected = false;
/**
* Create Redis client with error handling
* @returns {Object} Redis client instance
*/
function createRedisClient() {
const redisClient = redis.createClient({
socket: {
host: REDIS_CONFIG.host,
port: REDIS_CONFIG.port,
connectTimeout: REDIS_CONFIG.connectTimeout,
commandTimeout: REDIS_CONFIG.commandTimeout,
reconnectStrategy: (retries) => {
if (retries > 10) {
logger.error('Redis reconnection failed after 10 attempts');
return new Error('Redis reconnection failed');
}
const delay = Math.min(retries * 50, 2000);
logger.warn(`Redis reconnecting in ${delay}ms (attempt ${retries})`);
return delay;
}
},
password: REDIS_CONFIG.password,
database: REDIS_CONFIG.db,
});
// Connection event handlers
redisClient.on('connect', () => {
logger.info('Redis client connected');
});
redisClient.on('ready', () => {
isConnected = true;
logger.info('Redis client ready', {
host: REDIS_CONFIG.host,
port: REDIS_CONFIG.port,
database: REDIS_CONFIG.db
});
});
redisClient.on('error', (error) => {
isConnected = false;
logger.error('Redis client error:', {
message: error.message,
code: error.code,
stack: error.stack
});
});
redisClient.on('end', () => {
isConnected = false;
logger.info('Redis client connection ended');
});
redisClient.on('reconnecting', () => {
logger.info('Redis client reconnecting...');
});
return redisClient;
}
/**
* Initialize Redis connection
* @returns {Promise<Object>} Redis client instance
*/
async function initializeRedis() {
try {
if (client && isConnected) {
logger.info('Redis already connected');
return client;
}
client = createRedisClient();
await client.connect();
// Test connection
const pong = await client.ping();
if (pong !== 'PONG') {
throw new Error('Redis ping test failed');
}
logger.info('Redis initialized successfully');
return client;
} catch (error) {
logger.error('Failed to initialize Redis:', {
host: REDIS_CONFIG.host,
port: REDIS_CONFIG.port,
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* Get Redis client instance
* @returns {Object|null} Redis client or null if not connected
*/
function getRedisClient() {
if (!client || !isConnected) {
logger.warn('Redis client requested but not connected');
return null;
}
return client;
}
/**
* Check if Redis is connected
* @returns {boolean} Connection status
*/
function isRedisConnected() {
return isConnected && client !== null;
}
/**
* Close Redis connection gracefully
* @returns {Promise<void>}
*/
async function closeRedis() {
try {
if (client && isConnected) {
await client.quit();
client = null;
isConnected = false;
logger.info('Redis connection closed gracefully');
}
} catch (error) {
logger.error('Error closing Redis connection:', error);
// Force close if graceful close fails
if (client) {
await client.disconnect();
client = null;
isConnected = false;
}
throw error;
}
}
/**
* Redis utility functions for common operations
*/
const RedisUtils = {
/**
* Set a key-value pair with optional expiration
* @param {string} key - Redis key
* @param {string} value - Value to store
* @param {number} ttl - Time to live in seconds (optional)
* @returns {Promise<string>} Redis response
*/
async set(key, value, ttl = null) {
const redisClient = getRedisClient();
if (!redisClient) throw new Error('Redis not connected');
try {
if (ttl) {
return await redisClient.setEx(key, ttl, value);
}
return await redisClient.set(key, value);
} catch (error) {
logger.error('Redis SET error:', { key, error: error.message });
throw error;
}
},
/**
* Get value by key
* @param {string} key - Redis key
* @returns {Promise<string|null>} Value or null if not found
*/
async get(key) {
const redisClient = getRedisClient();
if (!redisClient) throw new Error('Redis not connected');
try {
return await redisClient.get(key);
} catch (error) {
logger.error('Redis GET error:', { key, error: error.message });
throw error;
}
},
/**
* Delete a key
* @param {string} key - Redis key
* @returns {Promise<number>} Number of keys deleted
*/
async del(key) {
const redisClient = getRedisClient();
if (!redisClient) throw new Error('Redis not connected');
try {
return await redisClient.del(key);
} catch (error) {
logger.error('Redis DEL error:', { key, error: error.message });
throw error;
}
},
/**
* Check if key exists
* @param {string} key - Redis key
* @returns {Promise<boolean>} True if key exists
*/
async exists(key) {
const redisClient = getRedisClient();
if (!redisClient) throw new Error('Redis not connected');
try {
const result = await redisClient.exists(key);
return result === 1;
} catch (error) {
logger.error('Redis EXISTS error:', { key, error: error.message });
throw error;
}
}
};
module.exports = {
initializeRedis,
getRedisClient,
isRedisConnected,
closeRedis,
RedisUtils,
client: () => client // For backward compatibility
};

321
src/config/websocket.js Normal file
View file

@ -0,0 +1,321 @@
/**
* WebSocket Configuration and Connection Management
* Handles Socket.IO server initialization and connection management
*/
const { Server } = require('socket.io');
const logger = require('../utils/logger');
// Configuration
const WEBSOCKET_CONFIG = {
cors: {
origin: process.env.WEBSOCKET_CORS_ORIGIN?.split(',') || ['http://localhost:3000', 'http://localhost:3001'],
methods: ['GET', 'POST'],
credentials: true
},
pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT) || 20000,
pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL) || 25000,
maxHttpBufferSize: parseInt(process.env.WEBSOCKET_MAX_BUFFER_SIZE) || 1e6, // 1MB
transports: ['websocket', 'polling'],
allowEIO3: true,
compression: true,
httpCompression: true
};
let io = null;
let connectionCount = 0;
const connectedClients = new Map();
/**
* Initialize WebSocket server
* @param {Object} server - HTTP server instance
* @returns {Promise<Object>} Socket.IO server instance
*/
async function initializeWebSocket(server) {
try {
if (io) {
logger.info('WebSocket server already initialized');
return io;
}
// Create Socket.IO server
io = new Server(server, WEBSOCKET_CONFIG);
// Set up middleware for authentication and logging
io.use(async (socket, next) => {
const correlationId = socket.handshake.query.correlationId || require('uuid').v4();
socket.correlationId = correlationId;
logger.info('WebSocket connection attempt', {
correlationId,
socketId: socket.id,
ip: socket.handshake.address,
userAgent: socket.handshake.headers['user-agent']
});
next();
});
// Connection event handler
io.on('connection', (socket) => {
connectionCount++;
connectedClients.set(socket.id, {
connectedAt: new Date(),
ip: socket.handshake.address,
userAgent: socket.handshake.headers['user-agent'],
playerId: null, // Will be set after authentication
rooms: new Set()
});
logger.info('WebSocket client connected', {
correlationId: socket.correlationId,
socketId: socket.id,
totalConnections: connectionCount,
ip: socket.handshake.address
});
// Set up event handlers
setupSocketEventHandlers(socket);
// Handle disconnection
socket.on('disconnect', (reason) => {
connectionCount--;
const clientInfo = connectedClients.get(socket.id);
connectedClients.delete(socket.id);
logger.info('WebSocket client disconnected', {
correlationId: socket.correlationId,
socketId: socket.id,
reason,
totalConnections: connectionCount,
playerId: clientInfo?.playerId,
connectionDuration: clientInfo ? Date.now() - clientInfo.connectedAt : 0
});
});
// Handle connection errors
socket.on('error', (error) => {
logger.error('WebSocket connection error', {
correlationId: socket.correlationId,
socketId: socket.id,
error: error.message,
stack: error.stack
});
});
});
// Server-level error handling
io.engine.on('connection_error', (error) => {
logger.error('WebSocket connection error:', {
message: error.message,
code: error.code,
context: error.context
});
});
logger.info('WebSocket server initialized successfully', {
maxConnections: process.env.WEBSOCKET_MAX_CONNECTIONS || 'unlimited',
pingTimeout: WEBSOCKET_CONFIG.pingTimeout,
pingInterval: WEBSOCKET_CONFIG.pingInterval
});
return io;
} catch (error) {
logger.error('Failed to initialize WebSocket server:', error);
throw error;
}
}
/**
* Set up event handlers for individual socket connections
* @param {Object} socket - Socket.IO socket instance
*/
function setupSocketEventHandlers(socket) {
// Player authentication
socket.on('authenticate', async (data) => {
try {
logger.info('WebSocket authentication attempt', {
correlationId: socket.correlationId,
socketId: socket.id,
playerId: data?.playerId
});
// TODO: Implement JWT token validation
// For now, just acknowledge
socket.emit('authenticated', {
success: true,
message: 'Authentication successful'
});
// Update client information
if (connectedClients.has(socket.id)) {
connectedClients.get(socket.id).playerId = data?.playerId;
}
} catch (error) {
logger.error('WebSocket authentication error', {
correlationId: socket.correlationId,
socketId: socket.id,
error: error.message
});
socket.emit('authentication_error', {
success: false,
message: 'Authentication failed'
});
}
});
// Join room (for game features like galaxy regions, player groups, etc.)
socket.on('join_room', (roomName) => {
if (typeof roomName !== 'string' || roomName.length > 50) {
socket.emit('error', { message: 'Invalid room name' });
return;
}
socket.join(roomName);
const clientInfo = connectedClients.get(socket.id);
if (clientInfo) {
clientInfo.rooms.add(roomName);
}
logger.info('Client joined room', {
correlationId: socket.correlationId,
socketId: socket.id,
room: roomName,
playerId: clientInfo?.playerId
});
socket.emit('room_joined', { room: roomName });
});
// Leave room
socket.on('leave_room', (roomName) => {
socket.leave(roomName);
const clientInfo = connectedClients.get(socket.id);
if (clientInfo) {
clientInfo.rooms.delete(roomName);
}
logger.info('Client left room', {
correlationId: socket.correlationId,
socketId: socket.id,
room: roomName,
playerId: clientInfo?.playerId
});
socket.emit('room_left', { room: roomName });
});
// Ping/pong for connection testing
socket.on('ping', () => {
socket.emit('pong', { timestamp: Date.now() });
});
// Generic message handler (for debugging)
socket.on('message', (data) => {
logger.debug('WebSocket message received', {
correlationId: socket.correlationId,
socketId: socket.id,
data: typeof data === 'object' ? JSON.stringify(data) : data
});
});
}
/**
* Get WebSocket server instance
* @returns {Object|null} Socket.IO server instance
*/
function getWebSocketServer() {
return io;
}
/**
* Get connection statistics
* @returns {Object} Connection statistics
*/
function getConnectionStats() {
return {
totalConnections: connectionCount,
authenticatedConnections: Array.from(connectedClients.values())
.filter(client => client.playerId).length,
anonymousConnections: Array.from(connectedClients.values())
.filter(client => !client.playerId).length,
rooms: io ? Array.from(io.sockets.adapter.rooms.keys()) : []
};
}
/**
* Broadcast message to all connected clients
* @param {string} event - Event name
* @param {Object} data - Data to broadcast
*/
function broadcastToAll(event, data) {
if (!io) {
logger.warn('Attempted to broadcast but WebSocket server not initialized');
return;
}
io.emit(event, data);
logger.info('Broadcast sent to all clients', {
event,
recipientCount: connectionCount
});
}
/**
* Broadcast message to specific room
* @param {string} room - Room name
* @param {string} event - Event name
* @param {Object} data - Data to broadcast
*/
function broadcastToRoom(room, event, data) {
if (!io) {
logger.warn('Attempted to broadcast to room but WebSocket server not initialized');
return;
}
io.to(room).emit(event, data);
logger.info('Broadcast sent to room', {
room,
event,
recipientCount: io.sockets.adapter.rooms.get(room)?.size || 0
});
}
/**
* Close WebSocket server gracefully
* @returns {Promise<void>}
*/
async function closeWebSocket() {
if (!io) return;
try {
// Disconnect all clients
io.disconnectSockets();
// Close server
io.close();
io = null;
connectionCount = 0;
connectedClients.clear();
logger.info('WebSocket server closed gracefully');
} catch (error) {
logger.error('Error closing WebSocket server:', error);
throw error;
}
}
module.exports = {
initializeWebSocket,
getWebSocketServer,
getConnectionStats,
broadcastToAll,
broadcastToRoom,
closeWebSocket
};

View file

@ -0,0 +1,261 @@
/**
* Admin Authentication Controller
* Handles admin login, authentication, and admin-specific endpoints
*/
const AdminService = require('../../services/user/AdminService');
const { asyncHandler } = require('../../middleware/error.middleware');
const logger = require('../../utils/logger');
const adminService = new AdminService();
/**
* Admin login
* POST /api/admin/auth/login
*/
const login = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const { email, password } = req.body;
logger.info('Admin login request received', {
correlationId,
email
});
const authResult = await adminService.authenticateAdmin({
email,
password
}, correlationId);
logger.audit('Admin login successful', {
correlationId,
adminId: authResult.admin.id,
email: authResult.admin.email,
username: authResult.admin.username,
permissions: authResult.admin.permissions
});
// Set refresh token as httpOnly cookie
res.cookie('adminRefreshToken', authResult.tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 8 * 60 * 60 * 1000, // 8 hours (shorter than player tokens)
path: '/api/admin' // Restrict to admin routes
});
res.status(200).json({
success: true,
message: 'Admin login successful',
data: {
admin: authResult.admin,
accessToken: authResult.tokens.accessToken
},
correlationId
});
});
/**
* Admin logout
* POST /api/admin/auth/logout
*/
const logout = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const adminId = req.user?.adminId;
logger.audit('Admin logout request received', {
correlationId,
adminId
});
// Clear refresh token cookie
res.clearCookie('adminRefreshToken', {
path: '/api/admin'
});
// TODO: Add token to blacklist if implementing token blacklisting
logger.audit('Admin logout successful', {
correlationId,
adminId
});
res.status(200).json({
success: true,
message: 'Admin logout successful',
correlationId
});
});
/**
* Get current admin profile
* GET /api/admin/auth/me
*/
const getProfile = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const adminId = req.user.adminId;
logger.info('Admin profile request received', {
correlationId,
adminId
});
const profile = await adminService.getAdminProfile(adminId, correlationId);
logger.info('Admin profile retrieved', {
correlationId,
adminId,
username: profile.username
});
res.status(200).json({
success: true,
message: 'Admin profile retrieved successfully',
data: {
admin: profile
},
correlationId
});
});
/**
* Verify admin token (for testing/debugging)
* GET /api/admin/auth/verify
*/
const verifyToken = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const user = req.user;
logger.audit('Admin token verification request received', {
correlationId,
adminId: user.adminId,
username: user.username,
permissions: user.permissions
});
res.status(200).json({
success: true,
message: 'Admin token is valid',
data: {
admin: {
adminId: user.adminId,
email: user.email,
username: user.username,
permissions: user.permissions,
type: user.type,
tokenIssuedAt: new Date(user.iat * 1000),
tokenExpiresAt: new Date(user.exp * 1000)
}
},
correlationId
});
});
/**
* Refresh admin access token
* POST /api/admin/auth/refresh
*/
const refresh = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const refreshToken = req.cookies.adminRefreshToken;
if (!refreshToken) {
logger.warn('Admin token refresh request without refresh token', {
correlationId
});
return res.status(401).json({
success: false,
message: 'Admin refresh token not provided',
correlationId
});
}
// TODO: Implement admin refresh token validation and new token generation
logger.warn('Admin token refresh requested but not implemented', {
correlationId
});
res.status(501).json({
success: false,
message: 'Admin token refresh feature not yet implemented',
correlationId
});
});
/**
* Get system statistics (admin dashboard)
* GET /api/admin/auth/stats
*/
const getSystemStats = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const adminId = req.user.adminId;
logger.audit('System statistics request received', {
correlationId,
adminId
});
const stats = await adminService.getSystemStats(correlationId);
logger.audit('System statistics retrieved', {
correlationId,
adminId,
totalPlayers: stats.players.total,
activePlayers: stats.players.active
});
res.status(200).json({
success: true,
message: 'System statistics retrieved successfully',
data: {
stats
},
correlationId
});
});
/**
* Change admin password
* POST /api/admin/auth/change-password
*/
const changePassword = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const adminId = req.user.adminId;
const { currentPassword, newPassword } = req.body;
logger.audit('Admin password change request received', {
correlationId,
adminId
});
// TODO: Implement admin password change functionality
// This would involve:
// 1. Verify current password
// 2. Validate new password strength
// 3. Hash new password
// 4. Update in database
// 5. Optionally invalidate existing tokens
// 6. Send notification email
logger.warn('Admin password change requested but not implemented', {
correlationId,
adminId
});
res.status(501).json({
success: false,
message: 'Admin password change feature not yet implemented',
correlationId
});
});
module.exports = {
login,
logout,
getProfile,
verifyToken,
refresh,
getSystemStats,
changePassword
};

View file

@ -0,0 +1,298 @@
/**
* Player Authentication Controller
* Handles player registration, login, and authentication-related endpoints
*/
const PlayerService = require('../../services/user/PlayerService');
const { asyncHandler } = require('../../middleware/error.middleware');
const logger = require('../../utils/logger');
const playerService = new PlayerService();
/**
* Register a new player
* POST /api/auth/register
*/
const register = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const { email, username, password } = req.body;
logger.info('Player registration request received', {
correlationId,
email,
username
});
const player = await playerService.registerPlayer({
email,
username,
password
}, correlationId);
logger.info('Player registration successful', {
correlationId,
playerId: player.id,
email: player.email,
username: player.username
});
res.status(201).json({
success: true,
message: 'Player registered successfully',
data: {
player
},
correlationId
});
});
/**
* Player login
* POST /api/auth/login
*/
const login = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const { email, password } = req.body;
logger.info('Player login request received', {
correlationId,
email
});
const authResult = await playerService.authenticatePlayer({
email,
password
}, correlationId);
logger.info('Player login successful', {
correlationId,
playerId: authResult.player.id,
email: authResult.player.email,
username: authResult.player.username
});
// Set refresh token as httpOnly cookie
res.cookie('refreshToken', authResult.tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.status(200).json({
success: true,
message: 'Login successful',
data: {
player: authResult.player,
accessToken: authResult.tokens.accessToken
},
correlationId
});
});
/**
* Player logout
* POST /api/auth/logout
*/
const logout = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user?.playerId;
logger.info('Player logout request received', {
correlationId,
playerId
});
// Clear refresh token cookie
res.clearCookie('refreshToken');
// TODO: Add token to blacklist if implementing token blacklisting
logger.info('Player logout successful', {
correlationId,
playerId
});
res.status(200).json({
success: true,
message: 'Logout successful',
correlationId
});
});
/**
* Refresh access token
* POST /api/auth/refresh
*/
const refresh = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
logger.warn('Token refresh request without refresh token', {
correlationId
});
return res.status(401).json({
success: false,
message: 'Refresh token not provided',
correlationId
});
}
// TODO: Implement refresh token validation and new token generation
// For now, return error indicating feature not implemented
logger.warn('Token refresh requested but not implemented', {
correlationId
});
res.status(501).json({
success: false,
message: 'Token refresh feature not yet implemented',
correlationId
});
});
/**
* Get current player profile
* GET /api/auth/me
*/
const getProfile = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
logger.info('Player profile request received', {
correlationId,
playerId
});
const profile = await playerService.getPlayerProfile(playerId, correlationId);
logger.info('Player profile retrieved', {
correlationId,
playerId,
username: profile.username
});
res.status(200).json({
success: true,
message: 'Profile retrieved successfully',
data: {
player: profile
},
correlationId
});
});
/**
* Update current player profile
* PUT /api/auth/me
*/
const updateProfile = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
const updateData = req.body;
logger.info('Player profile update request received', {
correlationId,
playerId,
updateFields: Object.keys(updateData)
});
const updatedProfile = await playerService.updatePlayerProfile(
playerId,
updateData,
correlationId
);
logger.info('Player profile updated successfully', {
correlationId,
playerId,
username: updatedProfile.username
});
res.status(200).json({
success: true,
message: 'Profile updated successfully',
data: {
player: updatedProfile
},
correlationId
});
});
/**
* Verify player token (for testing/debugging)
* GET /api/auth/verify
*/
const verifyToken = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const user = req.user;
logger.info('Token verification request received', {
correlationId,
playerId: user.playerId,
username: user.username
});
res.status(200).json({
success: true,
message: 'Token is valid',
data: {
user: {
playerId: user.playerId,
email: user.email,
username: user.username,
type: user.type,
tokenIssuedAt: new Date(user.iat * 1000),
tokenExpiresAt: new Date(user.exp * 1000)
}
},
correlationId
});
});
/**
* Change player password
* POST /api/auth/change-password
*/
const changePassword = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
const { currentPassword, newPassword } = req.body;
logger.info('Password change request received', {
correlationId,
playerId
});
// TODO: Implement password change functionality
// This would involve:
// 1. Verify current password
// 2. Validate new password strength
// 3. Hash new password
// 4. Update in database
// 5. Optionally invalidate existing tokens
logger.warn('Password change requested but not implemented', {
correlationId,
playerId
});
res.status(501).json({
success: false,
message: 'Password change feature not yet implemented',
correlationId
});
});
module.exports = {
register,
login,
logout,
refresh,
getProfile,
updateProfile,
verifyToken,
changePassword
};

View file

@ -0,0 +1,307 @@
/**
* Player Management Controller
* Handles player-specific operations and game-related endpoints
*/
const PlayerService = require('../../services/user/PlayerService');
const { asyncHandler } = require('../../middleware/error.middleware');
const logger = require('../../utils/logger');
const playerService = new PlayerService();
/**
* Get player dashboard data
* GET /api/player/dashboard
*/
const getDashboard = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
logger.info('Player dashboard request received', {
correlationId,
playerId
});
// Get player profile with resources and stats
const profile = await playerService.getPlayerProfile(playerId, correlationId);
// TODO: Add additional dashboard data such as:
// - Recent activities
// - Colony summaries
// - Fleet statuses
// - Research progress
// - Messages/notifications
const dashboardData = {
player: profile,
summary: {
totalColonies: profile.stats.coloniesCount,
totalFleets: profile.stats.fleetsCount,
totalBattles: profile.stats.totalBattles,
winRate: profile.stats.totalBattles > 0
? Math.round((profile.stats.battlesWon / profile.stats.totalBattles) * 100)
: 0
},
// Placeholder for future dashboard sections
recentActivity: [],
notifications: [],
gameStatus: {
online: true,
lastTick: new Date().toISOString()
}
};
logger.info('Player dashboard data retrieved', {
correlationId,
playerId,
username: profile.username
});
res.status(200).json({
success: true,
message: 'Dashboard data retrieved successfully',
data: dashboardData,
correlationId
});
});
/**
* Get player resources
* GET /api/player/resources
*/
const getResources = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
logger.info('Player resources request received', {
correlationId,
playerId
});
const profile = await playerService.getPlayerProfile(playerId, correlationId);
logger.info('Player resources retrieved', {
correlationId,
playerId,
scrap: profile.resources.scrap,
energy: profile.resources.energy
});
res.status(200).json({
success: true,
message: 'Resources retrieved successfully',
data: {
resources: profile.resources,
lastUpdated: new Date().toISOString()
},
correlationId
});
});
/**
* Get player statistics
* GET /api/player/stats
*/
const getStats = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
logger.info('Player statistics request received', {
correlationId,
playerId
});
const profile = await playerService.getPlayerProfile(playerId, correlationId);
const detailedStats = {
...profile.stats,
winRate: profile.stats.totalBattles > 0
? Math.round((profile.stats.battlesWon / profile.stats.totalBattles) * 100)
: 0,
lossRate: profile.stats.totalBattles > 0
? Math.round(((profile.stats.totalBattles - profile.stats.battlesWon) / profile.stats.totalBattles) * 100)
: 0,
accountAge: Math.floor((Date.now() - new Date(profile.createdAt).getTime()) / (1000 * 60 * 60 * 24)) // days
};
logger.info('Player statistics retrieved', {
correlationId,
playerId,
totalBattles: detailedStats.totalBattles,
winRate: detailedStats.winRate
});
res.status(200).json({
success: true,
message: 'Statistics retrieved successfully',
data: {
stats: detailedStats,
lastUpdated: new Date().toISOString()
},
correlationId
});
});
/**
* Update player settings
* PUT /api/player/settings
*/
const updateSettings = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
const settings = req.body;
logger.info('Player settings update request received', {
correlationId,
playerId,
settingsKeys: Object.keys(settings)
});
// TODO: Implement player settings update
// This would involve:
// 1. Validate settings data
// 2. Update player_settings table
// 3. Return updated settings
logger.warn('Player settings update requested but not implemented', {
correlationId,
playerId
});
res.status(501).json({
success: false,
message: 'Player settings update feature not yet implemented',
correlationId
});
});
/**
* Get player activity log
* GET /api/player/activity
*/
const getActivity = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
const { page = 1, limit = 20 } = req.query;
logger.info('Player activity log request received', {
correlationId,
playerId,
page,
limit
});
// TODO: Implement player activity log retrieval
// This would show recent actions like:
// - Colony creations/updates
// - Fleet movements
// - Research completions
// - Battle results
// - Resource transactions
const mockActivity = {
activities: [],
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false
}
};
logger.info('Player activity log retrieved', {
correlationId,
playerId,
activitiesCount: mockActivity.activities.length
});
res.status(200).json({
success: true,
message: 'Activity log retrieved successfully',
data: mockActivity,
correlationId
});
});
/**
* Get player notifications
* GET /api/player/notifications
*/
const getNotifications = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
const { unreadOnly = false } = req.query;
logger.info('Player notifications request received', {
correlationId,
playerId,
unreadOnly
});
// TODO: Implement player notifications retrieval
// This would show:
// - System messages
// - Battle results
// - Research completions
// - Fleet arrival notifications
// - Player messages
const mockNotifications = {
notifications: [],
unreadCount: 0,
totalCount: 0
};
logger.info('Player notifications retrieved', {
correlationId,
playerId,
unreadCount: mockNotifications.unreadCount
});
res.status(200).json({
success: true,
message: 'Notifications retrieved successfully',
data: mockNotifications,
correlationId
});
});
/**
* Mark notifications as read
* PUT /api/player/notifications/read
*/
const markNotificationsRead = asyncHandler(async (req, res) => {
const correlationId = req.correlationId;
const playerId = req.user.playerId;
const { notificationIds } = req.body;
logger.info('Mark notifications read request received', {
correlationId,
playerId,
notificationCount: notificationIds?.length || 0
});
// TODO: Implement notification marking as read
logger.warn('Mark notifications read requested but not implemented', {
correlationId,
playerId
});
res.status(501).json({
success: false,
message: 'Mark notifications read feature not yet implemented',
correlationId
});
});
module.exports = {
getDashboard,
getResources,
getStats,
updateSettings,
getActivity,
getNotifications,
markNotificationsRead
};

View file

@ -0,0 +1,446 @@
/**
* WebSocket Connection Handler
* Manages WebSocket connections and real-time game events
*/
const { verifyPlayerToken, extractTokenFromHeader } = require('../../utils/jwt');
const logger = require('../../utils/logger');
/**
* Handle new WebSocket connection
* @param {Object} socket - Socket.IO socket instance
* @param {Object} io - Socket.IO server instance
*/
function handleConnection(socket, io) {
const correlationId = socket.correlationId;
logger.info('WebSocket connection established', {
correlationId,
socketId: socket.id,
ip: socket.handshake.address
});
// Set up authentication handler
socket.on('authenticate', async (data) => {
await handleAuthentication(socket, data, correlationId);
});
// Set up game event handlers
setupGameEventHandlers(socket, io, correlationId);
// Set up utility handlers
setupUtilityHandlers(socket, io, correlationId);
// Handle disconnection
socket.on('disconnect', (reason) => {
handleDisconnection(socket, reason, correlationId);
});
// Handle connection errors
socket.on('error', (error) => {
handleConnectionError(socket, error, correlationId);
});
}
/**
* Handle WebSocket authentication
* @param {Object} socket - Socket.IO socket instance
* @param {Object} data - Authentication data
* @param {string} correlationId - Connection correlation ID
*/
async function handleAuthentication(socket, data, correlationId) {
try {
const { token } = data;
if (!token) {
logger.warn('WebSocket authentication failed - no token provided', {
correlationId,
socketId: socket.id
});
socket.emit('authentication_error', {
success: false,
message: 'Authentication token required'
});
return;
}
// Verify the player token
const decoded = verifyPlayerToken(token);
// Store player information in socket
socket.playerId = decoded.playerId;
socket.username = decoded.username;
socket.email = decoded.email;
socket.authenticated = true;
// Join player-specific room
const playerRoom = `player:${decoded.playerId}`;
socket.join(playerRoom);
logger.info('WebSocket authentication successful', {
correlationId,
socketId: socket.id,
playerId: decoded.playerId,
username: decoded.username
});
socket.emit('authenticated', {
success: true,
message: 'Authentication successful',
player: {
id: decoded.playerId,
username: decoded.username,
email: decoded.email
}
});
// Send initial game state or notifications
await sendInitialGameState(socket, decoded.playerId, correlationId);
} catch (error) {
logger.warn('WebSocket authentication failed', {
correlationId,
socketId: socket.id,
error: error.message
});
socket.emit('authentication_error', {
success: false,
message: 'Authentication failed'
});
}
}
/**
* Set up game event handlers
* @param {Object} socket - Socket.IO socket instance
* @param {Object} io - Socket.IO server instance
* @param {string} correlationId - Connection correlation ID
*/
function setupGameEventHandlers(socket, io, correlationId) {
// Colony updates
socket.on('subscribe_colony_updates', (data) => {
if (!socket.authenticated) {
socket.emit('error', { message: 'Authentication required' });
return;
}
const { colonyId } = data;
if (colonyId) {
const roomName = `colony:${colonyId}`;
socket.join(roomName);
logger.debug('Player subscribed to colony updates', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
colonyId,
room: roomName
});
socket.emit('subscribed', {
type: 'colony_updates',
colonyId: colonyId
});
}
});
// Fleet updates
socket.on('subscribe_fleet_updates', (data) => {
if (!socket.authenticated) {
socket.emit('error', { message: 'Authentication required' });
return;
}
const { fleetId } = data;
if (fleetId) {
const roomName = `fleet:${fleetId}`;
socket.join(roomName);
logger.debug('Player subscribed to fleet updates', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
fleetId,
room: roomName
});
socket.emit('subscribed', {
type: 'fleet_updates',
fleetId: fleetId
});
}
});
// Galaxy sector updates
socket.on('subscribe_sector_updates', (data) => {
if (!socket.authenticated) {
socket.emit('error', { message: 'Authentication required' });
return;
}
const { sectorId } = data;
if (sectorId) {
const roomName = `sector:${sectorId}`;
socket.join(roomName);
logger.debug('Player subscribed to sector updates', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
sectorId,
room: roomName
});
socket.emit('subscribed', {
type: 'sector_updates',
sectorId: sectorId
});
}
});
// Battle updates
socket.on('subscribe_battle_updates', (data) => {
if (!socket.authenticated) {
socket.emit('error', { message: 'Authentication required' });
return;
}
const { battleId } = data;
if (battleId) {
const roomName = `battle:${battleId}`;
socket.join(roomName);
logger.debug('Player subscribed to battle updates', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
battleId,
room: roomName
});
socket.emit('subscribed', {
type: 'battle_updates',
battleId: battleId
});
}
});
// Unsubscribe from updates
socket.on('unsubscribe', (data) => {
const { type, id } = data;
const roomName = `${type}:${id}`;
socket.leave(roomName);
logger.debug('Player unsubscribed from updates', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
type,
id,
room: roomName
});
socket.emit('unsubscribed', { type, id });
});
}
/**
* Set up utility handlers
* @param {Object} socket - Socket.IO socket instance
* @param {Object} io - Socket.IO server instance
* @param {string} correlationId - Connection correlation ID
*/
function setupUtilityHandlers(socket, io, correlationId) {
// Ping/pong for connection testing
socket.on('ping', (data) => {
const timestamp = Date.now();
socket.emit('pong', {
timestamp,
serverTime: new Date().toISOString(),
latency: data?.timestamp ? timestamp - data.timestamp : null
});
});
// Player status updates
socket.on('update_status', (data) => {
if (!socket.authenticated) {
socket.emit('error', { message: 'Authentication required' });
return;
}
const { status } = data;
if (['online', 'away', 'busy'].includes(status)) {
socket.playerStatus = status;
logger.debug('Player status updated', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
status
});
// Broadcast status to relevant rooms/players
// TODO: Implement player status broadcasting
}
});
// Chat/messaging
socket.on('send_message', async (data) => {
if (!socket.authenticated) {
socket.emit('error', { message: 'Authentication required' });
return;
}
// TODO: Implement real-time messaging
logger.debug('Message send requested', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
messageType: data.type
});
socket.emit('message_error', {
message: 'Messaging feature not yet implemented'
});
});
}
/**
* Handle WebSocket disconnection
* @param {Object} socket - Socket.IO socket instance
* @param {string} reason - Disconnection reason
* @param {string} correlationId - Connection correlation ID
*/
function handleDisconnection(socket, reason, correlationId) {
logger.info('WebSocket client disconnected', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
username: socket.username,
reason,
duration: socket.connectedAt ? Date.now() - socket.connectedAt : 0
});
// TODO: Update player online status
// TODO: Clean up any player-specific subscriptions or states
}
/**
* Handle connection errors
* @param {Object} socket - Socket.IO socket instance
* @param {Error} error - Error object
* @param {string} correlationId - Connection correlation ID
*/
function handleConnectionError(socket, error, correlationId) {
logger.error('WebSocket connection error', {
correlationId,
socketId: socket.id,
playerId: socket.playerId,
error: error.message,
stack: error.stack
});
socket.emit('connection_error', {
message: 'Connection error occurred',
reconnect: true
});
}
/**
* Send initial game state to newly authenticated player
* @param {Object} socket - Socket.IO socket instance
* @param {number} playerId - Player ID
* @param {string} correlationId - Connection correlation ID
*/
async function sendInitialGameState(socket, playerId, correlationId) {
try {
// TODO: Fetch and send initial game state
// This could include:
// - Player resources
// - Colony statuses
// - Fleet positions
// - Pending notifications
// - Current research
// - Active battles
const initialState = {
timestamp: new Date().toISOString(),
player: {
id: playerId,
online: true
},
gameState: {
// Placeholder for game state data
tick: Date.now(),
version: process.env.npm_package_version || '0.1.0'
},
notifications: {
unread: 0,
recent: []
}
};
socket.emit('initial_state', initialState);
logger.debug('Initial game state sent', {
correlationId,
socketId: socket.id,
playerId
});
} catch (error) {
logger.error('Failed to send initial game state', {
correlationId,
socketId: socket.id,
playerId,
error: error.message
});
socket.emit('error', {
message: 'Failed to load initial game state'
});
}
}
/**
* Broadcast game event to relevant players
* @param {Object} io - Socket.IO server instance
* @param {string} eventType - Type of event
* @param {Object} eventData - Event data
* @param {Array} targetPlayers - Array of player IDs to notify
*/
function broadcastGameEvent(io, eventType, eventData, targetPlayers = []) {
const timestamp = new Date().toISOString();
const broadcastData = {
type: eventType,
data: eventData,
timestamp
};
if (targetPlayers.length > 0) {
// Send to specific players
targetPlayers.forEach(playerId => {
io.to(`player:${playerId}`).emit('game_event', broadcastData);
});
logger.debug('Game event broadcast to specific players', {
eventType,
playerCount: targetPlayers.length
});
} else {
// Broadcast to all authenticated players
io.emit('game_event', broadcastData);
logger.debug('Game event broadcast to all players', {
eventType
});
}
}
module.exports = {
handleConnection,
broadcastGameEvent
};

View file

@ -0,0 +1,83 @@
const knex = require('knex');
const knexConfig = require('../../knexfile');
const logger = require('../utils/logger');
const environment = process.env.NODE_ENV || 'development';
const config = knexConfig[environment];
if (!config) {
throw new Error(`No database configuration found for environment: ${environment}`);
}
const db = knex(config);
// Track connection state
let isConnected = false;
/**
* Initialize database connection and verify connectivity
* @returns {Promise<boolean>} Connection success status
*/
async function initializeDatabase() {
try {
if (isConnected) {
logger.info('Database already connected');
return true;
}
// Test database connection
await db.raw('SELECT 1');
isConnected = true;
logger.info('Database connection established successfully', {
environment,
host: config.connection.host,
database: config.connection.database,
pool: {
min: config.pool?.min || 0,
max: config.pool?.max || 10
}
});
return true;
} catch (error) {
logger.error('Failed to establish database connection', {
environment,
host: config.connection?.host,
database: config.connection?.database,
error: error.message,
stack: error.stack
});
throw error;
}
}
/**
* Get database connection status
* @returns {boolean} Connection status
*/
function isDbConnected() {
return isConnected;
}
/**
* Close database connection gracefully
* @returns {Promise<void>}
*/
async function closeDatabase() {
try {
if (db && isConnected) {
await db.destroy();
isConnected = false;
logger.info('Database connection closed');
}
} catch (error) {
logger.error('Error closing database connection:', error);
throw error;
}
}
module.exports = db;
module.exports.initializeDatabase = initializeDatabase;
module.exports.isDbConnected = isDbConnected;
module.exports.closeDatabase = closeDatabase;

View file

@ -0,0 +1,192 @@
exports.up = async function(knex) {
// System configuration with hot-reloading support
await knex.schema.createTable('system_config', (table) => {
table.increments('id').primary();
table.string('config_key', 100).unique().notNullable();
table.jsonb('config_value').notNullable();
table.string('config_type', 20).notNullable().checkIn(['string', 'number', 'boolean', 'json', 'array']);
table.text('description');
table.boolean('requires_restart').defaultTo(false);
table.boolean('is_public').defaultTo(false); // Can be exposed to client
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.integer('updated_by'); // Will reference admin_users(id) after table creation
});
// Game tick system with user grouping
await knex.schema.createTable('game_tick_config', (table) => {
table.increments('id').primary();
table.integer('tick_interval_ms').notNullable().defaultTo(60000);
table.integer('user_groups_count').notNullable().defaultTo(10);
table.integer('max_retry_attempts').notNullable().defaultTo(5);
table.integer('bonus_tick_threshold').notNullable().defaultTo(3);
table.boolean('is_active').defaultTo(true);
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
await knex.schema.createTable('game_tick_log', (table) => {
table.bigIncrements('id').primary();
table.bigInteger('tick_number').notNullable();
table.integer('user_group').notNullable();
table.timestamp('started_at').notNullable();
table.timestamp('completed_at');
table.string('status', 20).notNullable().checkIn(['running', 'completed', 'failed', 'retrying']);
table.integer('retry_count').defaultTo(0);
table.text('error_message');
table.integer('processed_players').defaultTo(0);
table.jsonb('performance_metrics');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.index(['tick_number']);
table.index(['user_group']);
table.index(['status']);
});
// Event system configuration
await knex.schema.createTable('event_types', (table) => {
table.increments('id').primary();
table.string('name', 100).unique().notNullable();
table.text('description');
table.string('trigger_type', 20).notNullable().checkIn(['admin', 'player', 'system', 'mixed']);
table.boolean('is_active').defaultTo(true);
table.jsonb('config_schema'); // JSON schema for event configuration
table.timestamp('created_at').defaultTo(knex.fn.now());
});
await knex.schema.createTable('event_instances', (table) => {
table.bigIncrements('id').primary();
table.integer('event_type_id').notNullable().references('id').inTable('event_types');
table.string('name', 200).notNullable();
table.text('description');
table.jsonb('config').notNullable();
table.timestamp('start_time');
table.timestamp('end_time');
table.string('status', 20).notNullable().checkIn(['scheduled', 'active', 'completed', 'cancelled']);
table.integer('created_by'); // Will reference admin_users(id) after table creation
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.index(['event_type_id']);
table.index(['status']);
table.index(['start_time']);
});
// Plugin system for extensibility
await knex.schema.createTable('plugins', (table) => {
table.increments('id').primary();
table.string('name', 100).unique().notNullable();
table.string('version', 20).notNullable();
table.text('description');
table.string('plugin_type', 50).notNullable(); // 'combat', 'event', 'resource', etc.
table.boolean('is_active').defaultTo(false);
table.jsonb('config');
table.jsonb('dependencies'); // Array of required plugins
table.jsonb('hooks'); // Available hook points
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
// Insert initial system configuration
await knex('system_config').insert([
{
config_key: 'game_tick_interval_ms',
config_value: JSON.stringify(60000),
config_type: 'number',
description: 'Game tick interval in milliseconds',
is_public: false,
},
{
config_key: 'max_user_groups',
config_value: JSON.stringify(10),
config_type: 'number',
description: 'Maximum number of user groups for tick processing',
is_public: false,
},
{
config_key: 'max_retry_attempts',
config_value: JSON.stringify(5),
config_type: 'number',
description: 'Maximum retry attempts for failed ticks',
is_public: false,
},
{
config_key: 'data_retention_days',
config_value: JSON.stringify(30),
config_type: 'number',
description: 'Default data retention period in days',
is_public: false,
},
{
config_key: 'max_colonies_per_player',
config_value: JSON.stringify(10),
config_type: 'number',
description: 'Maximum colonies a player can own',
is_public: true,
},
{
config_key: 'starting_resources',
config_value: JSON.stringify({ scrap: 1000, energy: 500, data_cores: 0, rare_elements: 0 }),
config_type: 'json',
description: 'Starting resources for new players',
is_public: false,
},
{
config_key: 'websocket_ping_interval',
config_value: JSON.stringify(30000),
config_type: 'number',
description: 'WebSocket ping interval in milliseconds',
is_public: false,
},
]);
// Insert initial game tick configuration
await knex('game_tick_config').insert({
tick_interval_ms: 60000,
user_groups_count: 10,
max_retry_attempts: 5,
bonus_tick_threshold: 3,
});
// Insert initial event types
await knex('event_types').insert([
{
name: 'galaxy_crisis',
description: 'Major galaxy-wide crisis events',
trigger_type: 'admin',
config_schema: JSON.stringify({ duration_hours: { type: 'number', min: 1, max: 168 } }),
},
{
name: 'discovery_event',
description: 'Random discovery events triggered by exploration',
trigger_type: 'player',
config_schema: JSON.stringify({ discovery_type: { type: 'string', enum: ['artifact', 'technology', 'resource'] } }),
},
{
name: 'faction_war',
description: 'Large-scale conflicts between factions',
trigger_type: 'mixed',
config_schema: JSON.stringify({ participating_factions: { type: 'array', items: { type: 'number' } } }),
},
]);
// Insert initial plugin for basic combat
await knex('plugins').insert({
name: 'basic_combat',
version: '1.0.0',
description: 'Basic instant combat resolution system',
plugin_type: 'combat',
is_active: true,
config: JSON.stringify({ damage_variance: 0.1, experience_gain: 1.0 }),
hooks: JSON.stringify(['pre_combat', 'post_combat', 'damage_calculation']),
});
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('plugins');
await knex.schema.dropTableIfExists('event_instances');
await knex.schema.dropTableIfExists('event_types');
await knex.schema.dropTableIfExists('game_tick_log');
await knex.schema.dropTableIfExists('game_tick_config');
await knex.schema.dropTableIfExists('system_config');
};

View file

@ -0,0 +1,91 @@
exports.up = async function(knex) {
// Admin users with role-based access
await knex.schema.createTable('admin_users', (table) => {
table.increments('id').primary();
table.string('username', 50).unique().notNullable();
table.string('email', 255).unique().notNullable();
table.string('password_hash', 255).notNullable();
table.string('role', 50).notNullable().defaultTo('moderator');
table.jsonb('permissions'); // Specific permissions array
table.boolean('is_active').defaultTo(true);
table.timestamp('last_login');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
// Player accounts
await knex.schema.createTable('players', (table) => {
table.increments('id').primary();
table.string('username', 50).unique().notNullable();
table.string('email', 255).unique().notNullable();
table.string('password_hash', 255).notNullable();
table.boolean('email_verified').defaultTo(false);
table.string('email_verification_token', 255);
table.string('reset_password_token', 255);
table.timestamp('reset_password_expires');
table.integer('user_group').notNullable().checkBetween([0, 9]);
table.boolean('is_active').defaultTo(true);
table.boolean('is_banned').defaultTo(false);
table.text('ban_reason');
table.timestamp('ban_expires');
table.timestamp('last_login');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.index(['user_group']);
table.index(['email']);
table.index(['username']);
});
// Player preferences and game settings
await knex.schema.createTable('player_settings', (table) => {
table.increments('id').primary();
table.integer('player_id').notNullable().references('id').inTable('players').onDelete('CASCADE');
table.string('setting_key', 100).notNullable();
table.jsonb('setting_value').notNullable();
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.unique(['player_id', 'setting_key']);
});
// WebSocket subscriptions for real-time updates
await knex.schema.createTable('player_subscriptions', (table) => {
table.bigIncrements('id').primary();
table.integer('player_id').notNullable().references('id').inTable('players').onDelete('CASCADE');
table.string('subscription_type', 50).notNullable(); // 'colony', 'fleet', 'battle', 'event', etc.
table.integer('resource_id'); // Specific resource ID (colony_id, fleet_id, etc.)
table.jsonb('filters'); // Additional filtering criteria
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('expires_at');
table.index(['player_id']);
table.index(['subscription_type']);
table.index(['expires_at']);
});
// Add foreign key constraints to system_config now that admin_users exists
await knex.schema.table('system_config', (table) => {
table.foreign('updated_by').references('id').inTable('admin_users');
});
await knex.schema.table('event_instances', (table) => {
table.foreign('created_by').references('id').inTable('admin_users');
});
// Create initial admin user (password should be changed immediately)
await knex('admin_users').insert({
username: 'admin',
email: 'admin@shatteredvoid.game',
password_hash: '$2b$10$example.hash.change.immediately',
role: 'administrator',
permissions: JSON.stringify(['all']),
});
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('player_subscriptions');
await knex.schema.dropTableIfExists('player_settings');
await knex.schema.dropTableIfExists('players');
await knex.schema.dropTableIfExists('admin_users');
};

View file

@ -0,0 +1,257 @@
exports.up = async function(knex) {
// Planet types with generation rules
await knex.schema.createTable('planet_types', (table) => {
table.increments('id').primary();
table.string('name', 50).unique().notNullable();
table.text('description');
table.jsonb('base_resources').notNullable(); // Starting resource deposits
table.jsonb('resource_modifiers'); // Production modifiers by resource type
table.integer('max_population');
table.jsonb('special_features'); // Array of special features
table.integer('rarity_weight').defaultTo(100); // For random generation
table.boolean('is_active').defaultTo(true);
});
// Galaxy sectors for organization
await knex.schema.createTable('galaxy_sectors', (table) => {
table.increments('id').primary();
table.string('name', 50).notNullable();
table.string('coordinates', 10).unique().notNullable(); // e.g., "A3"
table.text('description');
table.integer('danger_level').defaultTo(1).checkBetween([1, 10]);
table.jsonb('special_rules');
table.timestamp('created_at').defaultTo(knex.fn.now());
});
// Player colonies
await knex.schema.createTable('colonies', (table) => {
table.increments('id').primary();
table.integer('player_id').notNullable().references('id').inTable('players').onDelete('CASCADE');
table.string('name', 100).notNullable();
table.string('coordinates', 20).unique().notNullable(); // Format: "A3-91-X"
table.integer('sector_id').references('id').inTable('galaxy_sectors');
table.integer('planet_type_id').notNullable().references('id').inTable('planet_types');
table.integer('population').defaultTo(0).checkPositive();
table.integer('max_population').defaultTo(1000);
table.integer('morale').defaultTo(100).checkBetween([0, 100]);
table.integer('loyalty').defaultTo(100).checkBetween([0, 100]);
table.timestamp('founded_at').defaultTo(knex.fn.now());
table.timestamp('last_updated').defaultTo(knex.fn.now());
table.index(['player_id']);
table.index(['coordinates']);
table.index(['sector_id']);
});
// Building types with upgrade paths
await knex.schema.createTable('building_types', (table) => {
table.increments('id').primary();
table.string('name', 100).unique().notNullable();
table.text('description');
table.string('category', 50).notNullable(); // 'production', 'military', 'research', 'infrastructure'
table.integer('max_level').defaultTo(10);
table.jsonb('base_cost').notNullable(); // Resource costs for level 1
table.decimal('cost_multiplier', 3, 2).defaultTo(1.5); // Cost increase per level
table.jsonb('base_production'); // Resource production at level 1
table.decimal('production_multiplier', 3, 2).defaultTo(1.2); // Production increase per level
table.jsonb('prerequisites'); // Required buildings/research
table.jsonb('special_effects'); // Special abilities or bonuses
table.boolean('is_unique').defaultTo(false); // Only one per colony
table.boolean('is_active').defaultTo(true);
});
// Colony buildings
await knex.schema.createTable('colony_buildings', (table) => {
table.increments('id').primary();
table.integer('colony_id').notNullable().references('id').inTable('colonies').onDelete('CASCADE');
table.integer('building_type_id').notNullable().references('id').inTable('building_types');
table.integer('level').defaultTo(1).checkPositive();
table.integer('health_percentage').defaultTo(100).checkBetween([0, 100]);
table.boolean('is_under_construction').defaultTo(false);
table.timestamp('construction_started');
table.timestamp('construction_completes');
table.timestamp('last_production');
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
table.unique(['colony_id', 'building_type_id']);
table.index(['colony_id']);
table.index('construction_completes', undefined, 'btree', undefined, { predicate: 'is_under_construction = true' });
});
// Insert initial planet types
await knex('planet_types').insert([
{
name: 'Terran',
description: 'Earth-like worlds with balanced resources',
base_resources: JSON.stringify({ scrap: 500, energy: 300 }),
resource_modifiers: JSON.stringify({ scrap: 1.0, energy: 1.0, data_cores: 1.0, rare_elements: 1.0 }),
max_population: 10000,
rarity_weight: 100,
},
{
name: 'Industrial',
description: 'Former manufacturing worlds rich in salvageable materials',
base_resources: JSON.stringify({ scrap: 1000, energy: 200 }),
resource_modifiers: JSON.stringify({ scrap: 1.5, energy: 0.8, data_cores: 1.2, rare_elements: 0.9 }),
max_population: 8000,
rarity_weight: 80,
},
{
name: 'Research',
description: 'Academic worlds with data archives and laboratories',
base_resources: JSON.stringify({ scrap: 300, energy: 400, data_cores: 100 }),
resource_modifiers: JSON.stringify({ scrap: 0.7, energy: 1.1, data_cores: 2.0, rare_elements: 1.3 }),
max_population: 6000,
rarity_weight: 60,
},
{
name: 'Mining',
description: 'Resource extraction worlds with rare element deposits',
base_resources: JSON.stringify({ scrap: 400, energy: 250, rare_elements: 20 }),
resource_modifiers: JSON.stringify({ scrap: 1.1, energy: 0.9, data_cores: 0.8, rare_elements: 2.5 }),
max_population: 7000,
rarity_weight: 70,
},
{
name: 'Fortress',
description: 'Heavily fortified military installations',
base_resources: JSON.stringify({ scrap: 600, energy: 500 }),
resource_modifiers: JSON.stringify({ scrap: 1.2, energy: 1.3, data_cores: 1.1, rare_elements: 1.1 }),
max_population: 5000,
rarity_weight: 40,
},
{
name: 'Derelict',
description: 'Abandoned worlds with scattered ruins and mysteries',
base_resources: JSON.stringify({ scrap: 200, energy: 100, data_cores: 50, rare_elements: 10 }),
resource_modifiers: JSON.stringify({ scrap: 0.8, energy: 0.6, data_cores: 1.5, rare_elements: 1.8 }),
max_population: 3000,
rarity_weight: 30,
},
]);
// Insert initial building types
await knex('building_types').insert([
{
name: 'Command Center',
description: 'Central administration building that coordinates colony operations',
category: 'infrastructure',
max_level: 10,
base_cost: JSON.stringify({ scrap: 100, energy: 50 }),
base_production: JSON.stringify({}),
special_effects: JSON.stringify({ colony_slots: 1 }),
},
{
name: 'Salvage Yard',
description: 'Processes scrap metal and salvageable materials',
category: 'production',
max_level: 10,
base_cost: JSON.stringify({ scrap: 50, energy: 25 }),
base_production: JSON.stringify({ scrap: 10 }),
special_effects: JSON.stringify({}),
},
{
name: 'Power Plant',
description: 'Generates energy for colony operations',
category: 'production',
max_level: 10,
base_cost: JSON.stringify({ scrap: 75, energy: 0 }),
base_production: JSON.stringify({ energy: 8 }),
special_effects: JSON.stringify({}),
},
{
name: 'Research Lab',
description: 'Conducts technological research and development',
category: 'research',
max_level: 10,
base_cost: JSON.stringify({ scrap: 100, energy: 75, data_cores: 10 }),
base_production: JSON.stringify({}),
special_effects: JSON.stringify({ research_speed: 0.1 }),
},
{
name: 'Housing Complex',
description: 'Provides living space for colony population',
category: 'infrastructure',
max_level: 15,
base_cost: JSON.stringify({ scrap: 60, energy: 30 }),
base_production: JSON.stringify({}),
special_effects: JSON.stringify({ population_capacity: 100 }),
},
{
name: 'Defense Grid',
description: 'Automated defense systems protecting the colony',
category: 'military',
max_level: 10,
base_cost: JSON.stringify({ scrap: 150, energy: 100, rare_elements: 5 }),
base_production: JSON.stringify({}),
special_effects: JSON.stringify({ defense_rating: 50 }),
},
{
name: 'Data Archive',
description: 'Stores and processes information from data cores',
category: 'research',
max_level: 8,
base_cost: JSON.stringify({ scrap: 80, energy: 60, data_cores: 5 }),
base_production: JSON.stringify({ data_cores: 2 }),
special_effects: JSON.stringify({ research_bonus: 0.05 }),
},
{
name: 'Mining Facility',
description: 'Extracts rare elements from planetary deposits',
category: 'production',
max_level: 12,
base_cost: JSON.stringify({ scrap: 200, energy: 150, data_cores: 20 }),
base_production: JSON.stringify({ rare_elements: 1 }),
special_effects: JSON.stringify({}),
},
]);
// Insert initial galaxy sectors
await knex('galaxy_sectors').insert([
{
name: 'Core Worlds',
coordinates: 'A1',
description: 'The former heart of galactic civilization',
danger_level: 3,
},
{
name: 'Industrial Belt',
coordinates: 'B2',
description: 'Manufacturing and resource processing sector',
danger_level: 4,
},
{
name: 'Frontier Region',
coordinates: 'C3',
description: 'Outer rim territories with unexplored systems',
danger_level: 6,
},
{
name: 'Research Zone',
coordinates: 'D4',
description: 'Former academic and scientific installations',
danger_level: 2,
},
{
name: 'Contested Space',
coordinates: 'E5',
description: 'Battlegrounds of the great collapse',
danger_level: 8,
},
{
name: 'Dead Zone',
coordinates: 'F6',
description: 'Heavily damaged region with dangerous anomalies',
danger_level: 9,
},
]);
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('colony_buildings');
await knex.schema.dropTableIfExists('building_types');
await knex.schema.dropTableIfExists('colonies');
await knex.schema.dropTableIfExists('galaxy_sectors');
await knex.schema.dropTableIfExists('planet_types');
};

View file

@ -0,0 +1,93 @@
exports.up = async function(knex) {
// Resource types
await knex.schema.createTable('resource_types', (table) => {
table.increments('id').primary();
table.string('name', 50).unique().notNullable();
table.text('description');
table.string('category', 30).notNullable(); // 'basic', 'advanced', 'rare', 'currency'
table.integer('max_storage'); // NULL for unlimited
table.decimal('decay_rate', 5, 4); // Daily decay percentage
table.decimal('trade_value', 10, 2); // Base trade value
table.boolean('is_tradeable').defaultTo(true);
table.boolean('is_active').defaultTo(true);
});
// Player resource stockpiles
await knex.schema.createTable('player_resources', (table) => {
table.increments('id').primary();
table.integer('player_id').notNullable().references('id').inTable('players').onDelete('CASCADE');
table.integer('resource_type_id').notNullable().references('id').inTable('resource_types');
table.bigInteger('amount').defaultTo(0).checkPositive();
table.bigInteger('storage_capacity');
table.timestamp('last_updated').defaultTo(knex.fn.now());
table.unique(['player_id', 'resource_type_id']);
table.index(['player_id']);
});
// Colony resource production/consumption tracking
await knex.schema.createTable('colony_resource_production', (table) => {
table.bigIncrements('id').primary();
table.integer('colony_id').notNullable().references('id').inTable('colonies').onDelete('CASCADE');
table.integer('resource_type_id').notNullable().references('id').inTable('resource_types');
table.integer('production_rate').defaultTo(0); // Per hour
table.integer('consumption_rate').defaultTo(0); // Per hour
table.bigInteger('current_stored').defaultTo(0);
table.bigInteger('storage_capacity');
table.timestamp('last_calculated').defaultTo(knex.fn.now());
table.unique(['colony_id', 'resource_type_id']);
table.index(['colony_id']);
});
// Trade system
await knex.schema.createTable('trade_routes', (table) => {
table.increments('id').primary();
table.integer('from_colony_id').notNullable().references('id').inTable('colonies').onDelete('CASCADE');
table.integer('to_colony_id').notNullable().references('id').inTable('colonies').onDelete('CASCADE');
table.integer('resource_type_id').notNullable().references('id').inTable('resource_types');
table.integer('amount_per_trip').notNullable();
table.decimal('price_per_unit', 10, 2).notNullable();
table.string('status', 20).defaultTo('active').checkIn(['active', 'paused', 'cancelled']);
table.timestamp('created_at').defaultTo(knex.fn.now());
table.check('?? != ??', ['from_colony_id', 'to_colony_id']);
table.index(['from_colony_id']);
table.index(['to_colony_id']);
});
// Insert resource types
await knex('resource_types').insert([
{
name: 'scrap',
description: 'Basic salvaged materials from the ruins of civilization',
category: 'basic',
is_tradeable: true,
},
{
name: 'energy',
description: 'Power cells and energy storage units',
category: 'basic',
is_tradeable: true,
},
{
name: 'data_cores',
description: 'Advanced information storage containing pre-collapse knowledge',
category: 'advanced',
is_tradeable: true,
},
{
name: 'rare_elements',
description: 'Exotic materials required for advanced technologies',
category: 'rare',
is_tradeable: true,
},
]);
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('trade_routes');
await knex.schema.dropTableIfExists('colony_resource_production');
await knex.schema.dropTableIfExists('player_resources');
await knex.schema.dropTableIfExists('resource_types');
};

View file

@ -0,0 +1,316 @@
/**
* Initial data seeding for Shattered Void MMO
* Populates essential game data for development and testing
*/
exports.seed = async function(knex) {
console.log('Seeding initial game data...');
// Clear existing data (be careful in production!)
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
await knex('admin_users').del();
await knex('building_types').del();
await knex('ship_categories').del();
await knex('research_technologies').del();
}
// Insert default admin user
const adminUsers = [
{
username: 'admin',
email: 'admin@shatteredvoid.local',
password_hash: '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/0pHdCGOaG9hP2IjBK', // 'admin123'
role: 'super_admin',
permissions: JSON.stringify({
manage_players: true,
manage_events: true,
manage_system: true,
view_analytics: true,
}),
is_active: true,
},
];
await knex('admin_users').insert(adminUsers);
console.log('✓ Admin users seeded');
// Insert building types
const buildingTypes = [
{
name: 'Command Center',
description: 'Central command structure for colony management',
category: 'infrastructure',
max_level: 10,
base_build_time_minutes: 30,
base_cost_scrap: 500,
base_cost_energy: 250,
unique_per_colony: true,
provides_population_cap: 1000,
},
{
name: 'Scrap Processor',
description: 'Processes salvaged materials into usable scrap metal',
category: 'production',
max_level: 15,
base_build_time_minutes: 60,
base_cost_scrap: 200,
base_cost_energy: 100,
},
{
name: 'Energy Generator',
description: 'Generates energy cells from various power sources',
category: 'production',
max_level: 15,
base_build_time_minutes: 45,
base_cost_scrap: 300,
base_cost_energy: 50,
},
{
name: 'Data Archive',
description: 'Stores and processes information into data cores',
category: 'production',
max_level: 12,
base_build_time_minutes: 90,
base_cost_scrap: 400,
base_cost_energy: 200,
base_cost_data_cores: 10,
},
{
name: 'Mining Complex',
description: 'Extracts rare elements from planetary deposits',
category: 'production',
max_level: 10,
base_build_time_minutes: 120,
base_cost_scrap: 600,
base_cost_energy: 300,
base_cost_data_cores: 25,
},
{
name: 'Research Lab',
description: 'Conducts technological research and development',
category: 'research',
max_level: 20,
base_build_time_minutes: 180,
base_cost_scrap: 800,
base_cost_energy: 400,
base_cost_data_cores: 50,
},
{
name: 'Shipyard',
description: 'Constructs and maintains space vessels',
category: 'military',
max_level: 15,
base_build_time_minutes: 240,
base_cost_scrap: 1000,
base_cost_energy: 500,
base_cost_data_cores: 75,
unique_per_colony: true,
},
{
name: 'Defense Grid',
description: 'Automated defense system protecting the colony',
category: 'military',
max_level: 10,
base_build_time_minutes: 150,
base_cost_scrap: 750,
base_cost_energy: 350,
base_cost_rare_elements: 25,
},
];
await knex('building_types').insert(buildingTypes);
console.log('✓ Building types seeded');
// Insert building effects
const buildingEffects = [
// Scrap Processor production
{ building_type_id: 2, effect_type: 'production', resource_type: 'scrap', base_value: 50, scaling_per_level: 25 },
// Energy Generator production
{ building_type_id: 3, effect_type: 'production', resource_type: 'energy', base_value: 30, scaling_per_level: 15 },
// Data Archive production
{ building_type_id: 4, effect_type: 'production', resource_type: 'data_cores', base_value: 5, scaling_per_level: 3 },
// Mining Complex production
{ building_type_id: 5, effect_type: 'production', resource_type: 'rare_elements', base_value: 2, scaling_per_level: 1 },
];
await knex('building_effects').insert(buildingEffects);
console.log('✓ Building effects seeded');
// Insert ship categories
const shipCategories = [
{
name: 'Scout',
description: 'Fast, lightly armed reconnaissance vessel',
base_hull_points: 50,
base_speed: 20,
base_cargo_capacity: 10,
module_slots_light: 3,
module_slots_medium: 1,
module_slots_heavy: 0,
},
{
name: 'Frigate',
description: 'Balanced combat vessel with moderate capabilities',
base_hull_points: 150,
base_speed: 15,
base_cargo_capacity: 25,
module_slots_light: 4,
module_slots_medium: 2,
module_slots_heavy: 1,
},
{
name: 'Destroyer',
description: 'Heavy combat vessel with powerful weapons',
base_hull_points: 300,
base_speed: 10,
base_cargo_capacity: 15,
module_slots_light: 2,
module_slots_medium: 4,
module_slots_heavy: 2,
},
{
name: 'Transport',
description: 'Large cargo vessel with minimal combat capability',
base_hull_points: 100,
base_speed: 8,
base_cargo_capacity: 100,
module_slots_light: 2,
module_slots_medium: 1,
module_slots_heavy: 0,
},
];
await knex('ship_categories').insert(shipCategories);
console.log('✓ Ship categories seeded');
// Insert research technologies
const technologies = [
{
category_id: 1, // engineering
name: 'Advanced Materials',
description: 'Improved construction materials for stronger buildings',
level: 1,
base_research_cost: 100,
base_research_time_hours: 4,
prerequisites: JSON.stringify([]),
effects: JSON.stringify({ building_cost_reduction: 0.1 }),
},
{
category_id: 2, // physics
name: 'Fusion Power',
description: 'More efficient energy generation technology',
level: 1,
base_research_cost: 150,
base_research_time_hours: 6,
prerequisites: JSON.stringify([]),
effects: JSON.stringify({ energy_production_bonus: 0.25 }),
},
{
category_id: 3, // computing
name: 'Data Mining',
description: 'Advanced algorithms for information processing',
level: 1,
base_research_cost: 200,
base_research_time_hours: 8,
prerequisites: JSON.stringify([]),
effects: JSON.stringify({ data_core_production_bonus: 0.2 }),
},
{
category_id: 4, // military
name: 'Weapon Systems',
description: 'Basic military technology for ship weapons',
level: 1,
base_research_cost: 250,
base_research_time_hours: 10,
prerequisites: JSON.stringify([]),
effects: JSON.stringify({ combat_rating_bonus: 0.15 }),
},
];
await knex('research_technologies').insert(technologies);
console.log('✓ Research technologies seeded');
// Insert some test sectors and systems for development
if (process.env.NODE_ENV === 'development') {
const sectors = [
{
name: 'Sol Sector',
description: 'The remnants of humanity\'s birthplace',
x_coordinate: 0,
y_coordinate: 0,
sector_type: 'starting',
danger_level: 1,
resource_modifier: 1.0,
},
{
name: 'Alpha Centauri Sector',
description: 'First expansion zone with moderate resources',
x_coordinate: 1,
y_coordinate: 0,
sector_type: 'normal',
danger_level: 2,
resource_modifier: 1.1,
},
];
await knex('sectors').insert(sectors);
const systems = [
{
sector_id: 1,
name: 'Sol System',
x_coordinate: 0,
y_coordinate: 0,
star_type: 'main_sequence',
system_size: 8,
is_explored: true,
},
{
sector_id: 2,
name: 'Alpha Centauri A',
x_coordinate: 0,
y_coordinate: 0,
star_type: 'main_sequence',
system_size: 5,
is_explored: false,
},
];
await knex('star_systems').insert(systems);
const planets = [
{
system_id: 1,
name: 'Earth',
position: 3,
planet_type_id: 1, // terran
size: 150,
coordinates: 'SOL-03-E',
is_habitable: true,
},
{
system_id: 1,
name: 'Mars',
position: 4,
planet_type_id: 2, // desert
size: 80,
coordinates: 'SOL-04-M',
is_habitable: true,
},
{
system_id: 2,
name: 'Proxima b',
position: 1,
planet_type_id: 1, // terran
size: 120,
coordinates: 'ACA-01-P',
is_habitable: true,
},
];
await knex('planets').insert(planets);
console.log('✓ Test galaxy data seeded');
}
console.log('Initial data seeding completed successfully!');
};

View file

@ -0,0 +1,359 @@
/**
* Admin Authentication Middleware
* Handles JWT token validation and permission checking for admin endpoints
*/
const { verifyAdminToken, extractTokenFromHeader } = require('../utils/jwt');
const logger = require('../utils/logger');
/**
* Middleware to authenticate admin requests
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
async function authenticateAdmin(req, res, next) {
try {
const correlationId = req.correlationId;
// Extract token from Authorization header
const authHeader = req.get('Authorization');
const token = extractTokenFromHeader(authHeader);
if (!token) {
logger.warn('Admin authentication failed - no token provided', {
correlationId,
ip: req.ip,
userAgent: req.get('User-Agent'),
path: req.path
});
return res.status(401).json({
error: 'Authentication required',
message: 'No authentication token provided',
correlationId
});
}
// Verify the token
const decoded = verifyAdminToken(token);
// Add admin information to request object
req.user = {
adminId: decoded.adminId,
email: decoded.email,
username: decoded.username,
permissions: decoded.permissions || [],
type: 'admin',
iat: decoded.iat,
exp: decoded.exp
};
// Log admin access
logger.audit('Admin authenticated', {
correlationId,
adminId: decoded.adminId,
username: decoded.username,
permissions: decoded.permissions,
path: req.path,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
next();
} catch (error) {
const correlationId = req.correlationId;
logger.warn('Admin authentication failed', {
correlationId,
error: error.message,
ip: req.ip,
userAgent: req.get('User-Agent'),
path: req.path
});
let statusCode = 401;
let message = 'Invalid authentication token';
if (error.message === 'Token expired') {
statusCode = 401;
message = 'Authentication token has expired';
} else if (error.message === 'Invalid token') {
statusCode = 401;
message = 'Invalid authentication token';
}
return res.status(statusCode).json({
error: 'Authentication failed',
message,
correlationId
});
}
}
/**
* Middleware factory to check if admin has required permissions
* @param {string|Array<string>} requiredPermissions - Permission(s) required
* @returns {Function} Express middleware function
*/
function requirePermissions(requiredPermissions) {
// Normalize to array
const permissions = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];
return (req, res, next) => {
try {
const correlationId = req.correlationId;
const adminPermissions = req.user?.permissions || [];
const adminId = req.user?.adminId;
const username = req.user?.username;
if (!adminId) {
logger.warn('Permission check failed - no authenticated admin', {
correlationId,
requiredPermissions: permissions,
path: req.path
});
return res.status(401).json({
error: 'Authentication required',
message: 'Admin authentication required',
correlationId
});
}
// Check if admin has super admin permission (bypasses all checks)
if (adminPermissions.includes('super_admin')) {
logger.info('Permission check passed - super admin', {
correlationId,
adminId,
username,
requiredPermissions: permissions,
path: req.path
});
return next();
}
// Check if admin has all required permissions
const hasPermissions = permissions.every(permission =>
adminPermissions.includes(permission)
);
if (!hasPermissions) {
const missingPermissions = permissions.filter(permission =>
!adminPermissions.includes(permission)
);
logger.warn('Permission check failed - insufficient permissions', {
correlationId,
adminId,
username,
adminPermissions,
requiredPermissions: permissions,
missingPermissions,
path: req.path,
method: req.method
});
return res.status(403).json({
error: 'Insufficient permissions',
message: 'You do not have the required permissions to access this resource',
requiredPermissions: permissions,
correlationId
});
}
logger.info('Permission check passed', {
correlationId,
adminId,
username,
requiredPermissions: permissions,
path: req.path
});
next();
} catch (error) {
logger.error('Permission check error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
requiredPermissions: permissions
});
return res.status(500).json({
error: 'Internal server error',
message: 'Failed to verify permissions',
correlationId: req.correlationId
});
}
};
}
/**
* Middleware to check if admin can access player-specific resources
* Requires 'player_management' permission or ownership
* @param {string} paramName - Parameter name containing the player ID
* @returns {Function} Express middleware function
*/
function requirePlayerAccess(paramName = 'playerId') {
return (req, res, next) => {
try {
const correlationId = req.correlationId;
const adminPermissions = req.user?.permissions || [];
const adminId = req.user?.adminId;
const username = req.user?.username;
const targetPlayerId = req.params[paramName];
if (!adminId) {
return res.status(401).json({
error: 'Authentication required',
correlationId
});
}
// Super admin can access everything
if (adminPermissions.includes('super_admin')) {
return next();
}
// Check for player management permission
if (adminPermissions.includes('player_management')) {
logger.info('Player access granted - player management permission', {
correlationId,
adminId,
username,
targetPlayerId,
path: req.path
});
return next();
}
// Check for read-only player data permission for GET requests
if (req.method === 'GET' && adminPermissions.includes('player_data_read')) {
logger.info('Player access granted - read-only permission', {
correlationId,
adminId,
username,
targetPlayerId,
path: req.path
});
return next();
}
logger.warn('Player access denied - insufficient permissions', {
correlationId,
adminId,
username,
adminPermissions,
targetPlayerId,
path: req.path,
method: req.method
});
return res.status(403).json({
error: 'Insufficient permissions',
message: 'You do not have permission to access player data',
correlationId
});
} catch (error) {
logger.error('Player access check error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack
});
return res.status(500).json({
error: 'Internal server error',
message: 'Failed to verify player access permissions',
correlationId: req.correlationId
});
}
};
}
/**
* Middleware to log admin actions for audit purposes
* @param {string} action - Action being performed
* @returns {Function} Express middleware function
*/
function auditAdminAction(action) {
return (req, res, next) => {
try {
const correlationId = req.correlationId;
const adminId = req.user?.adminId;
const username = req.user?.username;
// Log the action
logger.audit('Admin action initiated', {
correlationId,
adminId,
username,
action,
path: req.path,
method: req.method,
params: req.params,
query: req.query,
ip: req.ip,
userAgent: req.get('User-Agent')
});
// Override res.json to log the response
const originalJson = res.json;
res.json = function(data) {
logger.audit('Admin action completed', {
correlationId,
adminId,
username,
action,
path: req.path,
method: req.method,
statusCode: res.statusCode,
success: res.statusCode < 400
});
return originalJson.call(this, data);
};
next();
} catch (error) {
logger.error('Admin audit logging error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
action
});
// Continue even if audit logging fails
next();
}
};
}
/**
* Common admin permission constants
*/
const ADMIN_PERMISSIONS = {
SUPER_ADMIN: 'super_admin',
PLAYER_MANAGEMENT: 'player_management',
PLAYER_DATA_READ: 'player_data_read',
SYSTEM_MANAGEMENT: 'system_management',
GAME_MANAGEMENT: 'game_management',
EVENT_MANAGEMENT: 'event_management',
ANALYTICS_READ: 'analytics_read',
CONTENT_MANAGEMENT: 'content_management'
};
module.exports = {
authenticateAdmin,
requirePermissions,
requirePlayerAccess,
auditAdminAction,
ADMIN_PERMISSIONS
};

210
src/middleware/auth.js Normal file
View file

@ -0,0 +1,210 @@
/**
* Authentication middleware for JWT token validation
*/
const jwt = require('jsonwebtoken');
const logger = require('../utils/logger');
const db = require('../database/connection');
/**
* Verify JWT token and attach user to request
* @param {string} userType - 'player' or 'admin'
* @returns {Function} Express middleware function
*/
function authenticateToken(userType = 'player') {
return async (req, res, next) => {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: 'Access token required',
code: 'TOKEN_MISSING',
});
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Check token type matches required type
if (decoded.type !== userType) {
return res.status(403).json({
error: 'Invalid token type',
code: 'INVALID_TOKEN_TYPE',
});
}
// Get user from database
const tableName = userType === 'admin' ? 'admin_users' : 'players';
const user = await db(tableName)
.where('id', decoded.userId)
.first();
if (!user) {
return res.status(403).json({
error: 'User not found',
code: 'USER_NOT_FOUND',
});
}
// Check if user is active
if (userType === 'player' && user.account_status !== 'active') {
return res.status(403).json({
error: 'Account is not active',
code: 'ACCOUNT_INACTIVE',
status: user.account_status,
});
}
if (userType === 'admin' && !user.is_active) {
return res.status(403).json({
error: 'Admin account is not active',
code: 'ADMIN_INACTIVE',
});
}
// Attach user to request
req.user = user;
req.token = decoded;
// Update last active timestamp for players
if (userType === 'player') {
// Don't await this to avoid slowing down requests
db('players')
.where('id', user.id)
.update({ last_active_at: new Date() })
.catch(error => {
logger.error('Failed to update last_active_at:', error);
});
}
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED',
});
} else if (error.name === 'JsonWebTokenError') {
return res.status(403).json({
error: 'Invalid token',
code: 'INVALID_TOKEN',
});
} else {
logger.error('Authentication error:', error);
return res.status(500).json({
error: 'Authentication failed',
code: 'AUTH_ERROR',
});
}
}
};
}
/**
* Optional authentication - sets user if token is provided but doesn't require it
* @param {string} userType - 'player' or 'admin'
* @returns {Function} Express middleware function
*/
function optionalAuth(userType = 'player') {
return async (req, res, next) => {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return next();
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (decoded.type === userType) {
const tableName = userType === 'admin' ? 'admin_users' : 'players';
const user = await db(tableName)
.where('id', decoded.userId)
.first();
if (user && ((userType === 'player' && user.account_status === 'active') ||
(userType === 'admin' && user.is_active))) {
req.user = user;
req.token = decoded;
}
}
} catch (error) {
// Ignore errors in optional auth
logger.debug('Optional auth failed:', error.message);
}
next();
};
}
/**
* Check if user has specific permission (for admin users)
* @param {string} permission - Required permission
* @returns {Function} Express middleware function
*/
function requirePermission(permission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Authentication required',
code: 'AUTH_REQUIRED',
});
}
// Super admins have all permissions
if (req.user.role === 'super_admin') {
return next();
}
// Check specific permission
const permissions = req.user.permissions || {};
if (!permissions[permission]) {
return res.status(403).json({
error: 'Insufficient permissions',
code: 'INSUFFICIENT_PERMISSIONS',
required: permission,
});
}
next();
};
}
/**
* Check if user has specific role
* @param {string|string[]} roles - Required role(s)
* @returns {Function} Express middleware function
*/
function requireRole(roles) {
const requiredRoles = Array.isArray(roles) ? roles : [roles];
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Authentication required',
code: 'AUTH_REQUIRED',
});
}
if (!requiredRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient role',
code: 'INSUFFICIENT_ROLE',
required: requiredRoles,
current: req.user.role,
});
}
next();
};
}
module.exports = {
authenticateToken,
optionalAuth,
requirePermission,
requireRole,
};

View file

@ -0,0 +1,260 @@
/**
* Player Authentication Middleware
* Handles JWT token validation for player endpoints
*/
const { verifyPlayerToken, extractTokenFromHeader } = require('../utils/jwt');
const logger = require('../utils/logger');
/**
* Middleware to authenticate player requests
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
async function authenticatePlayer(req, res, next) {
try {
const correlationId = req.correlationId;
// Extract token from Authorization header
const authHeader = req.get('Authorization');
const token = extractTokenFromHeader(authHeader);
if (!token) {
logger.warn('Player authentication failed - no token provided', {
correlationId,
ip: req.ip,
userAgent: req.get('User-Agent'),
path: req.path
});
return res.status(401).json({
error: 'Authentication required',
message: 'No authentication token provided',
correlationId
});
}
// Verify the token
const decoded = verifyPlayerToken(token);
// Add player information to request object
req.user = {
playerId: decoded.playerId,
email: decoded.email,
username: decoded.username,
type: 'player',
iat: decoded.iat,
exp: decoded.exp
};
logger.info('Player authenticated successfully', {
correlationId,
playerId: decoded.playerId,
username: decoded.username,
path: req.path,
method: req.method
});
next();
} catch (error) {
const correlationId = req.correlationId;
logger.warn('Player authentication failed', {
correlationId,
error: error.message,
ip: req.ip,
userAgent: req.get('User-Agent'),
path: req.path
});
let statusCode = 401;
let message = 'Invalid authentication token';
if (error.message === 'Token expired') {
statusCode = 401;
message = 'Authentication token has expired';
} else if (error.message === 'Invalid token') {
statusCode = 401;
message = 'Invalid authentication token';
}
return res.status(statusCode).json({
error: 'Authentication failed',
message,
correlationId
});
}
}
/**
* Optional player authentication middleware
* Authenticates if token is present, but doesn't require it
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
async function optionalPlayerAuth(req, res, next) {
try {
const authHeader = req.get('Authorization');
const token = extractTokenFromHeader(authHeader);
if (token) {
try {
const decoded = verifyPlayerToken(token);
req.user = {
playerId: decoded.playerId,
email: decoded.email,
username: decoded.username,
type: 'player',
iat: decoded.iat,
exp: decoded.exp
};
logger.info('Optional player authentication successful', {
correlationId: req.correlationId,
playerId: decoded.playerId,
username: decoded.username
});
} catch (error) {
logger.warn('Optional player authentication failed', {
correlationId: req.correlationId,
error: error.message
});
// Continue without authentication
}
}
next();
} catch (error) {
// If there's an unexpected error, log it but continue
logger.error('Optional player authentication error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack
});
next();
}
}
/**
* Middleware to check if authenticated user owns a resource
* @param {string} paramName - Parameter name containing the player ID
* @returns {Function} Express middleware function
*/
function requireOwnership(paramName = 'playerId') {
return (req, res, next) => {
try {
const correlationId = req.correlationId;
const authenticatedPlayerId = req.user?.playerId;
const resourcePlayerId = parseInt(req.params[paramName]);
if (!authenticatedPlayerId) {
logger.warn('Ownership check failed - no authenticated user', {
correlationId,
path: req.path
});
return res.status(401).json({
error: 'Authentication required',
message: 'You must be authenticated to access this resource',
correlationId
});
}
if (!resourcePlayerId || isNaN(resourcePlayerId)) {
logger.warn('Ownership check failed - invalid resource ID', {
correlationId,
paramName,
resourcePlayerId: req.params[paramName],
playerId: authenticatedPlayerId
});
return res.status(400).json({
error: 'Invalid request',
message: 'Invalid resource identifier',
correlationId
});
}
if (authenticatedPlayerId !== resourcePlayerId) {
logger.warn('Ownership check failed - access denied', {
correlationId,
authenticatedPlayerId,
resourcePlayerId,
username: req.user.username,
path: req.path
});
return res.status(403).json({
error: 'Access denied',
message: 'You can only access your own resources',
correlationId
});
}
logger.info('Ownership check passed', {
correlationId,
playerId: authenticatedPlayerId,
username: req.user.username,
path: req.path
});
next();
} catch (error) {
logger.error('Ownership check error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack
});
return res.status(500).json({
error: 'Internal server error',
message: 'Failed to verify resource ownership',
correlationId: req.correlationId
});
}
};
}
/**
* Middleware to extract player ID from token and add to params
* Useful for routes that should automatically use the authenticated player's ID
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function injectPlayerId(req, res, next) {
try {
if (req.user && req.user.playerId) {
req.params.playerId = req.user.playerId.toString();
logger.debug('Player ID injected into params', {
correlationId: req.correlationId,
playerId: req.user.playerId,
path: req.path
});
}
next();
} catch (error) {
logger.error('Player ID injection error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack
});
next(); // Continue even if injection fails
}
}
module.exports = {
authenticatePlayer,
optionalPlayerAuth,
requireOwnership,
injectPlayerId
};

46
src/middleware/cors.js Normal file
View file

@ -0,0 +1,46 @@
/**
* CORS configuration middleware
*/
const cors = require('cors');
// Configure CORS options
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, postman, etc.)
if (!origin) return callback(null, true);
// In development, allow any origin
if (process.env.NODE_ENV === 'development') {
return callback(null, true);
}
// In production, check against allowed origins
const allowedOrigins = (process.env.CORS_ORIGIN || 'http://localhost:3000').split(',');
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'X-Correlation-ID',
],
exposedHeaders: [
'X-Total-Count',
'X-Page-Count',
'X-Rate-Limit-Remaining',
'X-Rate-Limit-Reset',
],
maxAge: 86400, // 24 hours
};
module.exports = cors(corsOptions);

View file

@ -0,0 +1,269 @@
/**
* CORS Configuration Middleware
* Handles Cross-Origin Resource Sharing with environment-based configuration
*/
const cors = require('cors');
const logger = require('../utils/logger');
// CORS Configuration
const CORS_CONFIG = {
development: {
origin: [
'http://localhost:3000',
'http://localhost:3001',
'http://127.0.0.1:3000',
'http://127.0.0.1:3001'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'X-Correlation-ID'
],
exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'],
maxAge: 86400 // 24 hours
},
production: {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, etc.)
if (!origin) return callback(null, true);
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '').split(',').map(o => o.trim());
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
logger.warn('CORS origin blocked', { origin });
callback(new Error('Not allowed by CORS'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'X-Correlation-ID'
],
exposeddHeaders: ['X-Correlation-ID', 'X-Total-Count'],
maxAge: 3600 // 1 hour
},
test: {
origin: true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'X-Correlation-ID'
],
exposedHeaders: ['X-Correlation-ID', 'X-Total-Count']
}
};
/**
* Get CORS configuration for current environment
* @returns {Object} CORS configuration object
*/
function getCorsConfig() {
const env = process.env.NODE_ENV || 'development';
const config = CORS_CONFIG[env] || CORS_CONFIG.development;
// Override with environment variables if provided
if (process.env.CORS_ALLOWED_ORIGINS) {
const origins = process.env.CORS_ALLOWED_ORIGINS.split(',').map(o => o.trim());
config.origin = origins;
}
if (process.env.CORS_CREDENTIALS) {
config.credentials = process.env.CORS_CREDENTIALS === 'true';
}
if (process.env.CORS_MAX_AGE) {
config.maxAge = parseInt(process.env.CORS_MAX_AGE);
}
return config;
}
/**
* Create CORS middleware with logging
* @returns {Function} CORS middleware function
*/
function createCorsMiddleware() {
const config = getCorsConfig();
logger.info('CORS middleware configured', {
environment: process.env.NODE_ENV || 'development',
origins: typeof config.origin === 'function' ? 'dynamic' : config.origin,
credentials: config.credentials,
methods: config.methods
});
return cors({
...config,
// Override origin handler to add logging
origin: function(origin, callback) {
const correlationId = require('uuid').v4();
// Handle dynamic origin function
if (typeof config.origin === 'function') {
return config.origin(origin, (err, allowed) => {
if (err) {
logger.warn('CORS origin rejected', {
correlationId,
origin,
error: err.message
});
} else if (allowed) {
logger.debug('CORS origin allowed', {
correlationId,
origin
});
}
callback(err, allowed);
});
}
// Handle static origin configuration
if (config.origin === true) {
logger.debug('CORS origin allowed (wildcard)', {
correlationId,
origin
});
return callback(null, true);
}
if (Array.isArray(config.origin)) {
const allowed = config.origin.includes(origin);
if (allowed) {
logger.debug('CORS origin allowed', {
correlationId,
origin
});
} else {
logger.warn('CORS origin rejected', {
correlationId,
origin,
allowedOrigins: config.origin
});
}
return callback(null, allowed);
}
// Single origin string
if (config.origin === origin) {
logger.debug('CORS origin allowed', {
correlationId,
origin
});
return callback(null, true);
}
logger.warn('CORS origin rejected', {
correlationId,
origin,
allowedOrigin: config.origin
});
callback(new Error('Not allowed by CORS'));
}
});
}
/**
* Middleware to add security headers for CORS
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function addSecurityHeaders(req, res, next) {
// Add Vary header for proper caching
res.vary('Origin');
// Add security headers
res.set({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin'
});
// Log cross-origin requests
const origin = req.get('Origin');
if (origin && origin !== `${req.protocol}://${req.get('Host')}`) {
logger.debug('Cross-origin request', {
correlationId: req.correlationId,
origin,
method: req.method,
path: req.path,
userAgent: req.get('User-Agent')
});
}
next();
}
/**
* Handle preflight OPTIONS requests
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function handlePreflight(req, res, next) {
if (req.method === 'OPTIONS') {
logger.debug('CORS preflight request', {
correlationId: req.correlationId,
origin: req.get('Origin'),
requestedMethod: req.get('Access-Control-Request-Method'),
requestedHeaders: req.get('Access-Control-Request-Headers')
});
}
next();
}
/**
* CORS error handler
* @param {Error} err - CORS error
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function handleCorsError(err, req, res, next) {
if (err.message === 'Not allowed by CORS') {
logger.warn('CORS request blocked', {
correlationId: req.correlationId,
origin: req.get('Origin'),
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.status(403).json({
error: 'CORS Policy Violation',
message: 'Cross-origin requests are not allowed from this origin',
correlationId: req.correlationId
});
}
next(err);
}
// Create and export the configured CORS middleware
const corsMiddleware = createCorsMiddleware();
module.exports = corsMiddleware;

View file

@ -0,0 +1,283 @@
/**
* Global error handling middleware
*/
const logger = require('../utils/logger');
/**
* Custom error classes
*/
class ValidationError extends Error {
constructor(message, details = {}) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
this.details = details;
}
}
class ConflictError extends Error {
constructor(message, details = {}) {
super(message);
this.name = 'ConflictError';
this.statusCode = 409;
this.details = details;
}
}
class NotFoundError extends Error {
constructor(message = 'Resource not found', details = {}) {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
this.details = details;
}
}
class ForbiddenError extends Error {
constructor(message = 'Access forbidden', details = {}) {
super(message);
this.name = 'ForbiddenError';
this.statusCode = 403;
this.details = details;
}
}
class ServiceError extends Error {
constructor(message, originalError = null) {
super(message);
this.name = 'ServiceError';
this.statusCode = 500;
this.originalError = originalError;
}
}
class RateLimitError extends Error {
constructor(message = 'Rate limit exceeded', details = {}) {
super(message);
this.name = 'RateLimitError';
this.statusCode = 429;
this.details = details;
}
}
/**
* Global error handler middleware
* @param {Error} error - The error object
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function errorHandler(error, req, res, next) {
// Skip if response already sent
if (res.headersSent) {
return next(error);
}
// Default error response
let statusCode = error.statusCode || 500;
let errorResponse = {
error: error.message || 'Internal server error',
code: error.name || 'INTERNAL_ERROR',
timestamp: new Date().toISOString(),
};
// Add request ID if available
if (req.correlationId) {
errorResponse.requestId = req.correlationId;
}
// Handle specific error types
switch (error.name) {
case 'ValidationError':
statusCode = 400;
errorResponse.details = error.details;
logger.warn('Validation error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
details: error.details,
});
break;
case 'ConflictError':
statusCode = 409;
errorResponse.details = error.details;
logger.warn('Conflict error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
details: error.details,
});
break;
case 'NotFoundError':
statusCode = 404;
errorResponse.details = error.details;
logger.warn('Not found error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
});
break;
case 'ForbiddenError':
statusCode = 403;
errorResponse.details = error.details;
logger.warn('Forbidden error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
userId: req.user?.id,
});
break;
case 'RateLimitError':
statusCode = 429;
errorResponse.details = error.details;
logger.warn('Rate limit error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
ip: req.ip,
error: error.message,
});
break;
case 'JsonWebTokenError':
statusCode = 401;
errorResponse.error = 'Invalid authentication token';
errorResponse.code = 'INVALID_TOKEN';
logger.warn('JWT error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
});
break;
case 'TokenExpiredError':
statusCode = 401;
errorResponse.error = 'Authentication token expired';
errorResponse.code = 'TOKEN_EXPIRED';
logger.warn('JWT expired', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
});
break;
case 'CastError':
case 'ValidationError':
// Database validation errors
statusCode = 400;
errorResponse.error = 'Invalid data provided';
errorResponse.code = 'INVALID_DATA';
logger.warn('Database validation error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
});
break;
case 'ServiceError':
statusCode = 500;
logger.error('Service error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
originalError: error.originalError?.message,
stack: error.stack,
});
break;
default:
// Log unexpected errors
logger.error('Unhandled error', {
correlationId: req.correlationId,
path: req.path,
method: req.method,
error: error.message,
stack: error.stack,
name: error.name,
});
// Don't expose internal errors in production
if (process.env.NODE_ENV === 'production') {
errorResponse.error = 'Internal server error';
errorResponse.code = 'INTERNAL_ERROR';
}
break;
}
// Add stack trace in development
if (process.env.NODE_ENV === 'development') {
errorResponse.stack = error.stack;
}
// Add additional error details in development
if (process.env.NODE_ENV === 'development' && error.originalError) {
errorResponse.originalError = {
message: error.originalError.message,
stack: error.originalError.stack,
};
}
res.status(statusCode).json(errorResponse);
}
/**
* 404 handler for unmatched routes
*/
function notFoundHandler(req, res) {
const error = {
error: 'Route not found',
code: 'ROUTE_NOT_FOUND',
path: req.originalUrl,
method: req.method,
timestamp: new Date().toISOString(),
};
if (req.correlationId) {
error.requestId = req.correlationId;
}
logger.warn('Route not found', {
correlationId: req.correlationId,
path: req.originalUrl,
method: req.method,
ip: req.ip,
});
res.status(404).json(error);
}
/**
* Async error wrapper to catch promise rejections
* @param {Function} fn - Async function to wrap
* @returns {Function} Wrapped function
*/
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
module.exports = {
errorHandler,
notFoundHandler,
asyncHandler,
ValidationError,
ConflictError,
NotFoundError,
ForbiddenError,
ServiceError,
RateLimitError,
};

View file

@ -0,0 +1,479 @@
/**
* Global Error Handling Middleware
* Comprehensive error handling with proper logging, sanitization, and response formatting
*/
const logger = require('../utils/logger');
/**
* Custom error classes for better error handling
*/
class ValidationError extends Error {
constructor(message, details = null) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
this.details = details;
}
}
class AuthenticationError extends Error {
constructor(message = 'Authentication failed') {
super(message);
this.name = 'AuthenticationError';
this.statusCode = 401;
}
}
class AuthorizationError extends Error {
constructor(message = 'Access denied') {
super(message);
this.name = 'AuthorizationError';
this.statusCode = 403;
}
}
class NotFoundError extends Error {
constructor(message = 'Resource not found') {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
class ConflictError extends Error {
constructor(message = 'Resource conflict') {
super(message);
this.name = 'ConflictError';
this.statusCode = 409;
}
}
class RateLimitError extends Error {
constructor(message = 'Rate limit exceeded') {
super(message);
this.name = 'RateLimitError';
this.statusCode = 429;
}
}
class ServiceError extends Error {
constructor(message = 'Internal service error', originalError = null) {
super(message);
this.name = 'ServiceError';
this.statusCode = 500;
this.originalError = originalError;
}
}
class DatabaseError extends Error {
constructor(message = 'Database operation failed', originalError = null) {
super(message);
this.name = 'DatabaseError';
this.statusCode = 500;
this.originalError = originalError;
}
}
/**
* Main error handling middleware
* @param {Error} error - Error object
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function errorHandler(error, req, res, next) {
const correlationId = req.correlationId || 'unknown';
const startTime = Date.now();
// Don't handle if response already sent
if (res.headersSent) {
logger.error('Error occurred after response sent', {
correlationId,
error: error.message,
stack: error.stack
});
return next(error);
}
// Log the error
logError(error, req, correlationId);
// Determine error details
const errorResponse = createErrorResponse(error, req, correlationId);
// Set appropriate headers
res.set({
'Content-Type': 'application/json',
'X-Correlation-ID': correlationId
});
// Send error response
res.status(errorResponse.statusCode).json(errorResponse.body);
// Log response time for error handling
const duration = Date.now() - startTime;
logger.info('Error response sent', {
correlationId,
statusCode: errorResponse.statusCode,
duration: `${duration}ms`
});
}
/**
* Log error with appropriate detail level
* @param {Error} error - Error object
* @param {Object} req - Express request object
* @param {string} correlationId - Request correlation ID
*/
function logError(error, req, correlationId) {
const errorInfo = {
correlationId,
name: error.name,
message: error.message,
statusCode: error.statusCode || 500,
method: req.method,
url: req.originalUrl,
path: req.path,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.playerId || req.user?.adminId,
userType: req.user?.type,
timestamp: new Date().toISOString()
};
// Add stack trace for server errors
if (!error.statusCode || error.statusCode >= 500) {
errorInfo.stack = error.stack;
// Add original error if available
if (error.originalError) {
errorInfo.originalError = {
name: error.originalError.name,
message: error.originalError.message,
stack: error.originalError.stack
};
}
}
// Add request body for debugging (sanitized)
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
errorInfo.requestBody = sanitizeForLogging(req.body);
}
// Add query parameters
if (Object.keys(req.query).length > 0) {
errorInfo.queryParams = req.query;
}
// Determine log level
const statusCode = error.statusCode || 500;
if (statusCode >= 500) {
logger.error('Server error occurred', errorInfo);
} else if (statusCode >= 400) {
logger.warn('Client error occurred', errorInfo);
} else {
logger.info('Request completed with error', errorInfo);
}
// Audit sensitive errors
if (shouldAuditError(error, req)) {
logger.audit('Error occurred', {
...errorInfo,
audit: true
});
}
}
/**
* Create standardized error response
* @param {Error} error - Error object
* @param {Object} req - Express request object
* @param {string} correlationId - Request correlation ID
* @returns {Object} Error response object
*/
function createErrorResponse(error, req, correlationId) {
const statusCode = determineStatusCode(error);
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const baseResponse = {
error: true,
correlationId,
timestamp: new Date().toISOString()
};
// Handle different error types
switch (error.name) {
case 'ValidationError':
return {
statusCode: 400,
body: {
...baseResponse,
type: 'ValidationError',
message: 'Request validation failed',
details: error.details || error.message
}
};
case 'AuthenticationError':
return {
statusCode: 401,
body: {
...baseResponse,
type: 'AuthenticationError',
message: isProduction ? 'Authentication required' : error.message
}
};
case 'AuthorizationError':
return {
statusCode: 403,
body: {
...baseResponse,
type: 'AuthorizationError',
message: isProduction ? 'Access denied' : error.message
}
};
case 'NotFoundError':
return {
statusCode: 404,
body: {
...baseResponse,
type: 'NotFoundError',
message: error.message || 'Resource not found'
}
};
case 'ConflictError':
return {
statusCode: 409,
body: {
...baseResponse,
type: 'ConflictError',
message: error.message || 'Resource conflict'
}
};
case 'RateLimitError':
return {
statusCode: 429,
body: {
...baseResponse,
type: 'RateLimitError',
message: error.message || 'Rate limit exceeded',
retryAfter: error.retryAfter
}
};
// Database errors
case 'DatabaseError':
case 'SequelizeError':
case 'QueryFailedError':
return {
statusCode: 500,
body: {
...baseResponse,
type: 'DatabaseError',
message: isProduction ? 'Database operation failed' : error.message,
...(isDevelopment && { stack: error.stack })
}
};
// JWT errors
case 'JsonWebTokenError':
case 'TokenExpiredError':
case 'NotBeforeError':
return {
statusCode: 401,
body: {
...baseResponse,
type: 'TokenError',
message: 'Invalid or expired token'
}
};
// Multer errors (file upload)
case 'MulterError':
return {
statusCode: 400,
body: {
...baseResponse,
type: 'FileUploadError',
message: getMulterErrorMessage(error)
}
};
// Default server error
default:
return {
statusCode: statusCode >= 400 ? statusCode : 500,
body: {
...baseResponse,
type: 'ServerError',
message: isProduction ? 'Internal server error' : error.message,
...(isDevelopment && {
stack: error.stack,
originalError: error.originalError
})
}
};
}
}
/**
* Determine HTTP status code from error
* @param {Error} error - Error object
* @returns {number} HTTP status code
*/
function determineStatusCode(error) {
// Use explicit status code if available
if (error.statusCode && typeof error.statusCode === 'number') {
return error.statusCode;
}
// Use status property if available
if (error.status && typeof error.status === 'number') {
return error.status;
}
// Default mappings by error name
const statusMappings = {
'ValidationError': 400,
'CastError': 400,
'JsonWebTokenError': 401,
'TokenExpiredError': 401,
'UnauthorizedError': 401,
'AuthenticationError': 401,
'ForbiddenError': 403,
'AuthorizationError': 403,
'NotFoundError': 404,
'ConflictError': 409,
'MulterError': 400,
'RateLimitError': 429
};
return statusMappings[error.name] || 500;
}
/**
* Get user-friendly message for Multer errors
* @param {Error} error - Multer error
* @returns {string} User-friendly error message
*/
function getMulterErrorMessage(error) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
return 'File size too large';
case 'LIMIT_FILE_COUNT':
return 'Too many files uploaded';
case 'LIMIT_FIELD_KEY':
return 'Field name too long';
case 'LIMIT_FIELD_VALUE':
return 'Field value too long';
case 'LIMIT_FIELD_COUNT':
return 'Too many fields';
case 'LIMIT_UNEXPECTED_FILE':
return 'Unexpected file field';
default:
return 'File upload error';
}
}
/**
* Sanitize data for logging (remove sensitive information)
* @param {Object} data - Data to sanitize
* @returns {Object} Sanitized data
*/
function sanitizeForLogging(data) {
if (!data || typeof data !== 'object') return data;
try {
const sanitized = JSON.parse(JSON.stringify(data));
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization'];
function recursiveSanitize(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
Object.keys(obj).forEach(key => {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
recursiveSanitize(obj[key]);
}
});
return obj;
}
return recursiveSanitize(sanitized);
} catch {
return '[SANITIZATION_ERROR]';
}
}
/**
* Determine if error should be audited
* @param {Error} error - Error object
* @param {Object} req - Express request object
* @returns {boolean} True if should audit
*/
function shouldAuditError(error, req) {
const statusCode = error.statusCode || 500;
// Audit all server errors
if (statusCode >= 500) return true;
// Audit authentication/authorization errors
if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) {
return true;
}
// Audit admin-related errors
if (req.user?.type === 'admin') return true;
// Audit security-related endpoints
if (req.path.includes('/auth/') || req.path.includes('/admin/')) {
return true;
}
return false;
}
/**
* Handle async errors in route handlers
* @param {Function} fn - Async route handler function
* @returns {Function} Wrapped route handler
*/
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
/**
* 404 Not Found handler
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function notFoundHandler(req, res, next) {
const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`);
next(error);
}
module.exports = {
errorHandler,
notFoundHandler,
asyncHandler,
// Export error classes
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
RateLimitError,
ServiceError,
DatabaseError
};

View file

@ -0,0 +1,371 @@
/**
* Request/Response Logging Middleware
* Comprehensive logging for HTTP requests with performance tracking and audit trails
*/
const logger = require('../utils/logger');
const { performance } = require('perf_hooks');
/**
* Main request logging middleware
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function requestLogger(req, res, next) {
const startTime = performance.now();
const correlationId = req.correlationId;
// Extract request information
const requestInfo = {
correlationId,
method: req.method,
url: req.originalUrl || req.url,
path: req.path,
query: Object.keys(req.query).length > 0 ? req.query : undefined,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
contentType: req.get('Content-Type'),
contentLength: req.get('Content-Length'),
referrer: req.get('Referrer'),
origin: req.get('Origin'),
timestamp: new Date().toISOString()
};
// Log request start
logger.info('Request started', requestInfo);
// Store original methods to override
const originalSend = res.send;
const originalJson = res.json;
const originalEnd = res.end;
let responseBody = null;
let responseSent = false;
// Override res.send to capture response
res.send = function(data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalSend.call(this, data);
};
// Override res.json to capture JSON response
res.json = function(data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalJson.call(this, data);
};
// Override res.end to capture empty responses
res.end = function(data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalEnd.call(this, data);
};
/**
* Log the response details
*/
function logResponse() {
if (responseSent) return;
responseSent = true;
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
const statusCode = res.statusCode;
const responseInfo = {
correlationId,
method: req.method,
url: req.originalUrl || req.url,
statusCode,
duration: `${duration}ms`,
contentLength: res.get('Content-Length'),
contentType: res.get('Content-Type'),
timestamp: new Date().toISOString()
};
// Add user information if available
if (req.user) {
responseInfo.userId = req.user.playerId || req.user.adminId;
responseInfo.userType = req.user.type;
responseInfo.username = req.user.username;
}
// Determine log level based on status code
let logLevel = 'info';
if (statusCode >= 400 && statusCode < 500) {
logLevel = 'warn';
} else if (statusCode >= 500) {
logLevel = 'error';
}
// Add response body for errors (but sanitize sensitive data)
if (statusCode >= 400 && responseBody) {
responseInfo.responseBody = sanitizeResponseBody(responseBody);
}
// Log slow requests as warnings
if (duration > 5000) { // 5 seconds
logLevel = 'warn';
responseInfo.slow = true;
}
logger[logLevel]('Request completed', responseInfo);
// Log audit trail for sensitive operations
if (shouldAudit(req, statusCode)) {
logAuditTrail(req, res, duration, correlationId);
}
// Track performance metrics
trackPerformanceMetrics(req, res, duration);
}
next();
}
/**
* Sanitize response body to remove sensitive information
* @param {any} responseBody - Response body to sanitize
* @returns {any} Sanitized response body
*/
function sanitizeResponseBody(responseBody) {
if (!responseBody) return responseBody;
try {
let sanitized = responseBody;
// If it's a string, try to parse as JSON
if (typeof responseBody === 'string') {
try {
sanitized = JSON.parse(responseBody);
} catch {
return responseBody; // Return as-is if not JSON
}
}
// Remove sensitive fields
if (typeof sanitized === 'object') {
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash'];
const cloned = JSON.parse(JSON.stringify(sanitized));
function removeSensitiveFields(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
Object.keys(obj).forEach(key => {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
removeSensitiveFields(obj[key]);
}
});
return obj;
}
return removeSensitiveFields(cloned);
}
return sanitized;
} catch (error) {
return '[SANITIZATION_ERROR]';
}
}
/**
* Determine if request should be audited
* @param {Object} req - Express request object
* @param {number} statusCode - Response status code
* @returns {boolean} True if should audit
*/
function shouldAudit(req, statusCode) {
// Audit admin actions
if (req.user?.type === 'admin') {
return true;
}
// Audit authentication attempts
if (req.path.includes('/auth/') || req.path.includes('/login')) {
return true;
}
// Audit failed requests
if (statusCode >= 400) {
return true;
}
// Audit sensitive game actions
const sensitiveActions = [
'/colonies',
'/fleets',
'/research',
'/messages',
'/profile'
];
if (sensitiveActions.some(action => req.path.includes(action)) && req.method !== 'GET') {
return true;
}
return false;
}
/**
* Log audit trail for sensitive operations
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {number} duration - Request duration in milliseconds
* @param {string} correlationId - Request correlation ID
*/
function logAuditTrail(req, res, duration, correlationId) {
const auditInfo = {
correlationId,
event: 'api_request',
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
};
// Add user information
if (req.user) {
auditInfo.userId = req.user.playerId || req.user.adminId;
auditInfo.userType = req.user.type;
auditInfo.username = req.user.username;
}
// Add request parameters for POST/PUT/PATCH requests (sanitized)
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
auditInfo.requestBody = sanitizeRequestBody(req.body);
}
// Add query parameters
if (Object.keys(req.query).length > 0) {
auditInfo.queryParams = req.query;
}
logger.audit('Audit trail', auditInfo);
}
/**
* Sanitize request body for audit logging
* @param {Object} body - Request body to sanitize
* @returns {Object} Sanitized request body
*/
function sanitizeRequestBody(body) {
if (!body || typeof body !== 'object') return body;
try {
const sensitiveFields = ['password', 'oldPassword', 'newPassword', 'token', 'secret'];
const cloned = JSON.parse(JSON.stringify(body));
sensitiveFields.forEach(field => {
if (cloned[field]) {
cloned[field] = '[REDACTED]';
}
});
return cloned;
} catch {
return '[SANITIZATION_ERROR]';
}
}
/**
* Track performance metrics
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {number} duration - Request duration in milliseconds
*/
function trackPerformanceMetrics(req, res, duration) {
// Only track metrics for non-health check endpoints
if (req.path === '/health') return;
const metrics = {
endpoint: `${req.method} ${req.route?.path || req.path}`,
duration,
statusCode: res.statusCode,
timestamp: Date.now()
};
// Log slow requests
if (duration > 1000) { // 1 second
logger.warn('Slow request detected', {
correlationId: req.correlationId,
...metrics,
threshold: '1000ms'
});
}
// Log very slow requests as errors
if (duration > 10000) { // 10 seconds
logger.error('Very slow request detected', {
correlationId: req.correlationId,
...metrics,
threshold: '10000ms'
});
}
// TODO: Send metrics to monitoring system (Prometheus, DataDog, etc.)
// This would integrate with your monitoring infrastructure
}
/**
* Middleware to skip logging for specific paths
* @param {Array<string>} skipPaths - Array of paths to skip
* @returns {Function} Middleware function
*/
function skipLogging(skipPaths = ['/health', '/favicon.ico']) {
return (req, res, next) => {
const shouldSkip = skipPaths.some(path => req.path === path);
if (shouldSkip) {
return next();
}
return requestLogger(req, res, next);
};
}
/**
* Error logging middleware (for unhandled errors)
* @param {Error} error - Error object
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
function errorLogger(error, req, res, next) {
logger.error('Unhandled request error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.playerId || req.user?.adminId,
userType: req.user?.type
});
next(error);
}
module.exports = {
requestLogger,
skipLogging,
errorLogger,
sanitizeResponseBody,
sanitizeRequestBody
};

View file

@ -0,0 +1,320 @@
/**
* Rate Limiting Middleware
* Implements comprehensive rate limiting with Redis backend and flexible configuration
*/
const rateLimit = require('express-rate-limit');
const { getRedisClient } = require('../config/redis');
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
},
// 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
},
// 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
},
// 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
}
};
/**
* Create Redis store for rate limiting if Redis is available
* @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;
}
}
/**
* Create key generator for rate limiting
* @param {string} prefix - Key prefix
* @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}`;
};
}
/**
* Create rate limit handler
* @param {string} type - Rate limit type for logging
* @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';
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
});
};
}
/**
* Create skip function for rate limiting
* @param {Array<string>} skipPaths - Paths to skip rate limiting
* @param {Array<string>} skipIPs - IPs to skip rate limiting
* @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;
}
// Skip specified paths
if (skipPaths.some(path => req.path.startsWith(path))) {
return true;
}
// Skip specified IPs (for development/testing)
if (skipIPs.includes(ip)) {
return true;
}
// Skip if rate limiting is disabled
if (process.env.DISABLE_RATE_LIMITING === 'true') {
return true;
}
return false;
};
}
/**
* Create rate limiter middleware
* @param {string} type - Type of rate limiter
* @param {Object} customConfig - Custom configuration
* @returns {Function} Rate limiter middleware
*/
function createRateLimiter(type, customConfig = {}) {
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
});
// Log rate limiter creation
logger.info('Rate limiter created', {
type,
windowMs: config.windowMs,
max: config.max,
useRedis: !!store
});
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')
};
/**
* Middleware to add rate limit headers even when not limiting
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @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'
});
next();
}
/**
* Custom rate limiter for WebSocket connections
* @param {number} maxConnections - Maximum connections per IP
* @param {number} windowMs - Time window in milliseconds
* @returns {Function} WebSocket rate limiter function
*/
function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) {
const connections = new Map();
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);
}
// 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'));
}
// Add current connection
currentConnections.push(now);
connections.set(ip, currentConnections);
logger.debug('WebSocket connection allowed', {
ip,
connections: currentConnections.length,
maxConnections
});
next();
};
}
/**
* Middleware to apply different rate limits based on user type
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @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;
}
return limiter(req, res, next);
}
module.exports = {
rateLimiters,
createRateLimiter,
createWebSocketRateLimiter,
addRateLimitHeaders,
dynamicRateLimit,
RATE_LIMIT_CONFIG
};

View file

@ -0,0 +1,92 @@
/**
* Request logging middleware
*/
const { v4: uuidv4 } = require('uuid');
const logger = require('../utils/logger');
/**
* Request logging middleware
* Logs incoming requests and attaches correlation ID
*/
function requestLogger(req, res, next) {
// Generate correlation ID for request tracking
req.correlationId = uuidv4();
// Capture start time
const startTime = Date.now();
// Extract user info if available
const getUserInfo = () => {
if (req.user) {
return {
userId: req.user.id,
username: req.user.username || req.user.display_name,
userType: req.user.role ? 'admin' : 'player',
};
}
return {};
};
// Log request start
logger.info('Request started', {
correlationId: req.correlationId,
method: req.method,
url: req.originalUrl,
userAgent: req.get('User-Agent'),
ip: req.ip,
...getUserInfo(),
});
// Override res.json to log response
const originalJson = res.json;
res.json = function (body) {
const duration = Date.now() - startTime;
// Log request completion
logger.info('Request completed', {
correlationId: req.correlationId,
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration: `${duration}ms`,
...getUserInfo(),
});
// Log slow requests
if (duration > 1000) {
logger.warn('Slow request detected', {
correlationId: req.correlationId,
method: req.method,
url: req.originalUrl,
duration: `${duration}ms`,
});
}
return originalJson.call(this, body);
};
// Override res.send to log response
const originalSend = res.send;
res.send = function (body) {
const duration = Date.now() - startTime;
// Only log if not already logged by res.json
if (!res.jsonLogged) {
logger.info('Request completed', {
correlationId: req.correlationId,
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration: `${duration}ms`,
...getUserInfo(),
});
}
return originalSend.call(this, body);
};
next();
}
module.exports = requestLogger;

View file

@ -0,0 +1,310 @@
/**
* Request Validation Middleware
* Provides validation middleware using Joi schemas with comprehensive error handling
*/
const Joi = require('joi');
const logger = require('../utils/logger');
/**
* Middleware factory for validating request data using Joi schemas
* @param {Object} schema - Joi validation schema
* @param {string} source - Source of data to validate ('body', 'params', 'query', 'headers')
* @returns {Function} Express middleware function
*/
function validateRequest(schema, source = 'body') {
return (req, res, next) => {
try {
const correlationId = req.correlationId;
let dataToValidate;
// Get data based on source
switch (source) {
case 'body':
dataToValidate = req.body;
break;
case 'params':
dataToValidate = req.params;
break;
case 'query':
dataToValidate = req.query;
break;
case 'headers':
dataToValidate = req.headers;
break;
default:
logger.error('Invalid validation source specified', {
correlationId,
source,
path: req.path
});
return res.status(500).json({
error: 'Internal server error',
message: 'Invalid validation configuration',
correlationId
});
}
// Perform validation
const { error, value } = schema.validate(dataToValidate, {
abortEarly: false, // Return all validation errors
stripUnknown: true, // Remove unknown properties
convert: true // Convert values to correct types
});
if (error) {
const validationErrors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context?.value
}));
logger.warn('Request validation failed', {
correlationId,
source,
path: req.path,
method: req.method,
errors: validationErrors,
originalData: JSON.stringify(dataToValidate)
});
return res.status(400).json({
error: 'Validation failed',
message: 'Request data is invalid',
details: validationErrors,
correlationId
});
}
// Replace the original data with validated/sanitized data
switch (source) {
case 'body':
req.body = value;
break;
case 'params':
req.params = value;
break;
case 'query':
req.query = value;
break;
case 'headers':
req.headers = value;
break;
}
logger.debug('Request validation passed', {
correlationId,
source,
path: req.path
});
next();
} catch (error) {
logger.error('Validation middleware error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
source
});
return res.status(500).json({
error: 'Internal server error',
message: 'Validation processing failed',
correlationId: req.correlationId
});
}
};
}
/**
* Common validation schemas
*/
const commonSchemas = {
// Player ID parameter validation
playerId: Joi.object({
playerId: Joi.number().integer().min(1).required()
}),
// Pagination query validation
pagination: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20),
sortBy: Joi.string().valid('created_at', 'updated_at', 'name', 'id').default('created_at'),
sortOrder: Joi.string().valid('asc', 'desc').default('desc')
}),
// Player registration validation
playerRegistration: Joi.object({
email: Joi.string().email().max(320).required(),
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().min(8).max(128).required()
}),
// Player login validation
playerLogin: Joi.object({
email: Joi.string().email().max(320).required(),
password: Joi.string().min(1).max(128).required()
}),
// Admin login validation
adminLogin: Joi.object({
email: Joi.string().email().max(320).required(),
password: Joi.string().min(1).max(128).required()
}),
// Colony creation validation
colonyCreation: Joi.object({
name: Joi.string().min(3).max(50).required(),
coordinates: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(),
planet_type_id: Joi.number().integer().min(1).required()
}),
// Colony update validation
colonyUpdate: Joi.object({
name: Joi.string().min(3).max(50).optional()
}),
// Fleet creation validation
fleetCreation: Joi.object({
name: Joi.string().min(3).max(50).required(),
ships: Joi.array().items(
Joi.object({
design_id: Joi.number().integer().min(1).required(),
quantity: Joi.number().integer().min(1).max(1000).required()
})
).min(1).required()
}),
// Fleet movement validation
fleetMovement: Joi.object({
destination: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(),
mission_type: Joi.string().valid('move', 'attack', 'colonize', 'transport').required()
}),
// Research initiation validation
researchInitiation: Joi.object({
technology_id: Joi.number().integer().min(1).required()
}),
// Message sending validation
messageSend: Joi.object({
to_player_id: Joi.number().integer().min(1).required(),
subject: Joi.string().min(1).max(100).required(),
content: Joi.string().min(1).max(2000).required()
})
};
/**
* Pre-built validation middleware for common use cases
*/
const validators = {
// Parameter validators
validatePlayerId: validateRequest(commonSchemas.playerId, 'params'),
validatePagination: validateRequest(commonSchemas.pagination, 'query'),
// Authentication validators
validatePlayerRegistration: validateRequest(commonSchemas.playerRegistration, 'body'),
validatePlayerLogin: validateRequest(commonSchemas.playerLogin, 'body'),
validateAdminLogin: validateRequest(commonSchemas.adminLogin, 'body'),
// Game feature validators
validateColonyCreation: validateRequest(commonSchemas.colonyCreation, 'body'),
validateColonyUpdate: validateRequest(commonSchemas.colonyUpdate, 'body'),
validateFleetCreation: validateRequest(commonSchemas.fleetCreation, 'body'),
validateFleetMovement: validateRequest(commonSchemas.fleetMovement, 'body'),
validateResearchInitiation: validateRequest(commonSchemas.researchInitiation, 'body'),
validateMessageSend: validateRequest(commonSchemas.messageSend, 'body')
};
/**
* Custom validation helpers
*/
const validationHelpers = {
/**
* Create a custom validation schema for coordinates
* @param {boolean} required - Whether the field is required
* @returns {Joi.Schema} Joi schema for coordinates
*/
coordinatesSchema(required = true) {
let schema = Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/);
return required ? schema.required() : schema.optional();
},
/**
* Create a custom validation schema for player IDs
* @param {boolean} required - Whether the field is required
* @returns {Joi.Schema} Joi schema for player IDs
*/
playerIdSchema(required = true) {
let schema = Joi.number().integer().min(1);
return required ? schema.required() : schema.optional();
},
/**
* Create a custom validation schema for resource amounts
* @param {number} min - Minimum value (default: 0)
* @param {number} max - Maximum value (default: 999999999)
* @returns {Joi.Schema} Joi schema for resource amounts
*/
resourceAmountSchema(min = 0, max = 999999999) {
return Joi.number().integer().min(min).max(max);
},
/**
* Create a validation schema for arrays with custom item validation
* @param {Joi.Schema} itemSchema - Schema for array items
* @param {number} minItems - Minimum number of items
* @param {number} maxItems - Maximum number of items
* @returns {Joi.Schema} Joi schema for arrays
*/
arraySchema(itemSchema, minItems = 0, maxItems = 100) {
return Joi.array().items(itemSchema).min(minItems).max(maxItems);
}
};
/**
* Middleware to sanitize HTML content in request body
* @param {Array<string>} fields - Fields to sanitize
* @returns {Function} Express middleware function
*/
function sanitizeHTML(fields = []) {
return (req, res, next) => {
try {
if (!req.body || typeof req.body !== 'object') {
return next();
}
const { sanitizeHTML: sanitize } = require('../utils/validation');
fields.forEach(field => {
if (req.body[field] && typeof req.body[field] === 'string') {
req.body[field] = sanitize(req.body[field]);
}
});
next();
} catch (error) {
logger.error('HTML sanitization error', {
correlationId: req.correlationId,
error: error.message,
fields
});
return res.status(500).json({
error: 'Internal server error',
message: 'Request processing failed',
correlationId: req.correlationId
});
}
};
}
module.exports = {
validateRequest,
commonSchemas,
validators,
validationHelpers,
sanitizeHTML
};

401
src/routes/admin.js Normal file
View file

@ -0,0 +1,401 @@
/**
* Admin API Routes
* Defines all administrative API endpoints with proper authentication and permissions
*/
const express = require('express');
const router = express.Router();
// Import middleware
const { authenticateAdmin, requirePermissions, requirePlayerAccess, auditAdminAction, ADMIN_PERMISSIONS } = require('../middleware/admin.middleware');
const { rateLimiters } = require('../middleware/rateLimit.middleware');
const { validators, validateRequest } = require('../middleware/validation.middleware');
const corsMiddleware = require('../middleware/cors.middleware');
// Import controllers
const adminAuthController = require('../controllers/admin/auth.controller');
// Import services for direct admin operations
const AdminService = require('../services/user/AdminService');
const adminService = new AdminService();
// Apply CORS to all admin routes
router.use(corsMiddleware);
// Apply admin-specific rate limiting
router.use(rateLimiters.admin);
/**
* Admin API Status and Information
*/
router.get('/', (req, res) => {
res.json({
name: 'Shattered Void - Admin API',
version: process.env.npm_package_version || '0.1.0',
status: 'operational',
timestamp: new Date().toISOString(),
correlationId: req.correlationId,
endpoints: {
authentication: '/api/admin/auth',
players: '/api/admin/players',
system: '/api/admin/system',
events: '/api/admin/events',
analytics: '/api/admin/analytics'
},
note: 'Administrative access required for all endpoints'
});
});
/**
* Admin Authentication Routes
* /api/admin/auth/*
*/
const authRoutes = express.Router();
// Public admin authentication endpoints
authRoutes.post('/login',
rateLimiters.auth,
validators.validateAdminLogin,
auditAdminAction('admin_login'),
adminAuthController.login
);
// Protected admin authentication endpoints
authRoutes.post('/logout',
authenticateAdmin,
auditAdminAction('admin_logout'),
adminAuthController.logout
);
authRoutes.get('/me',
authenticateAdmin,
adminAuthController.getProfile
);
authRoutes.get('/verify',
authenticateAdmin,
adminAuthController.verifyToken
);
authRoutes.post('/refresh',
rateLimiters.auth,
adminAuthController.refresh
);
authRoutes.get('/stats',
authenticateAdmin,
requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]),
auditAdminAction('view_system_stats'),
adminAuthController.getSystemStats
);
authRoutes.post('/change-password',
authenticateAdmin,
rateLimiters.auth,
validateRequest(require('joi').object({
currentPassword: require('joi').string().required(),
newPassword: require('joi').string().min(8).max(128).required()
}), 'body'),
auditAdminAction('admin_password_change'),
adminAuthController.changePassword
);
// Mount admin authentication routes
router.use('/auth', authRoutes);
/**
* Player Management Routes
* /api/admin/players/*
*/
const playerRoutes = express.Router();
// All player management routes require authentication
playerRoutes.use(authenticateAdmin);
// Get players list
playerRoutes.get('/',
requirePermissions([ADMIN_PERMISSIONS.PLAYER_DATA_READ]),
validators.validatePagination,
validateRequest(require('joi').object({
search: require('joi').string().max(50).optional(),
activeOnly: require('joi').boolean().optional(),
sortBy: require('joi').string().valid('created_at', 'updated_at', 'username', 'email', 'last_login_at').default('created_at'),
sortOrder: require('joi').string().valid('asc', 'desc').default('desc')
}), 'query'),
auditAdminAction('list_players'),
async (req, res) => {
try {
const {
page = 1,
limit = 20,
search = '',
activeOnly = null,
sortBy = 'created_at',
sortOrder = 'desc'
} = req.query;
const result = await adminService.getPlayersList({
page: parseInt(page),
limit: parseInt(limit),
search,
activeOnly,
sortBy,
sortOrder
}, req.correlationId);
res.json({
success: true,
message: 'Players list retrieved successfully',
data: result,
correlationId: req.correlationId
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to retrieve players list',
message: error.message,
correlationId: req.correlationId
});
}
}
);
// Get specific player details
playerRoutes.get('/:playerId',
requirePlayerAccess('playerId'),
validators.validatePlayerId,
auditAdminAction('view_player_details'),
async (req, res) => {
try {
const playerId = parseInt(req.params.playerId);
const playerDetails = await adminService.getPlayerDetails(playerId, req.correlationId);
res.json({
success: true,
message: 'Player details retrieved successfully',
data: {
player: playerDetails
},
correlationId: req.correlationId
});
} catch (error) {
const statusCode = error.name === 'NotFoundError' ? 404 : 500;
res.status(statusCode).json({
success: false,
error: error.name === 'NotFoundError' ? 'Player not found' : 'Failed to retrieve player details',
message: error.message,
correlationId: req.correlationId
});
}
}
);
// Update player status (activate/deactivate)
playerRoutes.put('/:playerId/status',
requirePermissions([ADMIN_PERMISSIONS.PLAYER_MANAGEMENT]),
validators.validatePlayerId,
validateRequest(require('joi').object({
isActive: require('joi').boolean().required(),
reason: require('joi').string().max(200).optional()
}), 'body'),
auditAdminAction('update_player_status'),
async (req, res) => {
try {
const playerId = parseInt(req.params.playerId);
const { isActive, reason } = req.body;
const updatedPlayer = await adminService.updatePlayerStatus(
playerId,
isActive,
req.correlationId
);
res.json({
success: true,
message: `Player ${isActive ? 'activated' : 'deactivated'} successfully`,
data: {
player: updatedPlayer,
action: isActive ? 'activated' : 'deactivated',
reason: reason || null
},
correlationId: req.correlationId
});
} catch (error) {
const statusCode = error.name === 'NotFoundError' ? 404 : 500;
res.status(statusCode).json({
success: false,
error: error.name === 'NotFoundError' ? 'Player not found' : 'Failed to update player status',
message: error.message,
correlationId: req.correlationId
});
}
}
);
// Mount player management routes
router.use('/players', playerRoutes);
/**
* System Management Routes
* /api/admin/system/*
*/
const systemRoutes = express.Router();
// All system routes require authentication
systemRoutes.use(authenticateAdmin);
// Get detailed system statistics
systemRoutes.get('/stats',
requirePermissions([ADMIN_PERMISSIONS.SYSTEM_MANAGEMENT]),
auditAdminAction('view_detailed_system_stats'),
async (req, res) => {
try {
const stats = await adminService.getSystemStats(req.correlationId);
// Add additional system information
const systemInfo = {
...stats,
server: {
version: process.env.npm_package_version || '0.1.0',
environment: process.env.NODE_ENV || 'development',
uptime: process.uptime(),
nodeVersion: process.version,
memory: {
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
rss: Math.round(process.memoryUsage().rss / 1024 / 1024)
}
}
};
res.json({
success: true,
message: 'System statistics retrieved successfully',
data: systemInfo,
correlationId: req.correlationId
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to retrieve system statistics',
message: error.message,
correlationId: req.correlationId
});
}
}
);
// System health check
systemRoutes.get('/health',
requirePermissions([ADMIN_PERMISSIONS.SYSTEM_MANAGEMENT]),
async (req, res) => {
try {
// TODO: Implement comprehensive health checks
// - Database connectivity
// - Redis connectivity
// - WebSocket server status
// - External service connectivity
const healthStatus = {
status: 'healthy',
timestamp: new Date().toISOString(),
services: {
database: 'healthy',
redis: 'healthy',
websocket: 'healthy'
},
performance: {
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: process.cpuUsage()
}
};
res.json({
success: true,
message: 'System health check completed',
data: healthStatus,
correlationId: req.correlationId
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Health check failed',
message: error.message,
correlationId: req.correlationId
});
}
}
);
// Mount system routes
router.use('/system', systemRoutes);
/**
* Events Management Routes (placeholder)
* /api/admin/events/*
*/
router.get('/events',
authenticateAdmin,
requirePermissions([ADMIN_PERMISSIONS.EVENT_MANAGEMENT]),
validators.validatePagination,
auditAdminAction('view_events'),
(req, res) => {
res.json({
success: true,
message: 'Events endpoint - feature not yet implemented',
data: {
events: [],
pagination: {
page: 1,
limit: 20,
total: 0,
totalPages: 0
}
},
correlationId: req.correlationId
});
}
);
/**
* Analytics Routes (placeholder)
* /api/admin/analytics/*
*/
router.get('/analytics',
authenticateAdmin,
requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]),
auditAdminAction('view_analytics'),
(req, res) => {
res.json({
success: true,
message: 'Analytics endpoint - feature not yet implemented',
data: {
analytics: {},
timeRange: 'daily',
metrics: []
},
correlationId: req.correlationId
});
}
);
/**
* Error handling for admin routes
*/
router.use('*', (req, res) => {
res.status(404).json({
success: false,
error: 'Admin API endpoint not found',
message: `The endpoint ${req.method} ${req.originalUrl} does not exist`,
correlationId: req.correlationId,
timestamp: new Date().toISOString()
});
});
module.exports = router;

View file

0
src/routes/admin/auth.js Normal file
View file

View file

42
src/routes/admin/index.js Normal file
View file

@ -0,0 +1,42 @@
/**
* Admin API routes
*/
const express = require('express');
const { authenticateToken, requireRole, requirePermission } = require('../../middleware/auth');
const { asyncHandler } = require('../../middleware/error-handler');
const router = express.Router();
// Import route modules
const authRoutes = require('./auth');
const systemRoutes = require('./system');
const playersRoutes = require('./players');
const eventsRoutes = require('./events');
const analyticsRoutes = require('./analytics');
// Admin authentication routes
router.use('/auth', authRoutes);
// Protected admin routes
router.use('/system', authenticateToken('admin'), requireRole(['admin', 'super_admin']), systemRoutes);
router.use('/players', authenticateToken('admin'), requirePermission('manage_players'), playersRoutes);
router.use('/events', authenticateToken('admin'), requirePermission('manage_events'), eventsRoutes);
router.use('/analytics', authenticateToken('admin'), requireRole(['admin', 'super_admin']), analyticsRoutes);
// Admin status endpoint
router.get('/status', authenticateToken('admin'), asyncHandler(async (req, res) => {
res.json({
status: 'authenticated',
admin: {
id: req.user.id,
username: req.user.username,
role: req.user.role,
permissions: req.user.permissions || {},
lastLogin: req.user.last_login_at,
},
timestamp: new Date().toISOString(),
});
}));
module.exports = router;

View file

View file

344
src/routes/api.js Normal file
View file

@ -0,0 +1,344 @@
/**
* Player API Routes
* Defines all player-facing API endpoints with proper middleware and validation
*/
const express = require('express');
const router = express.Router();
// Import middleware
const { authenticatePlayer, optionalPlayerAuth, requireOwnership, injectPlayerId } = require('../middleware/auth.middleware');
const { rateLimiters } = require('../middleware/rateLimit.middleware');
const { validators, validateRequest } = require('../middleware/validation.middleware');
const corsMiddleware = require('../middleware/cors.middleware');
// Import controllers
const authController = require('../controllers/api/auth.controller');
const playerController = require('../controllers/api/player.controller');
// Apply CORS to all API routes
router.use(corsMiddleware);
// Apply general API rate limiting
router.use(rateLimiters.player);
/**
* API Status and Information
*/
router.get('/', (req, res) => {
res.json({
name: 'Shattered Void - Player API',
version: process.env.npm_package_version || '0.1.0',
status: 'operational',
timestamp: new Date().toISOString(),
correlationId: req.correlationId,
endpoints: {
authentication: '/api/auth',
player: '/api/player',
game: {
colonies: '/api/colonies',
fleets: '/api/fleets',
research: '/api/research',
galaxy: '/api/galaxy'
}
}
});
});
/**
* Authentication Routes
* /api/auth/*
*/
const authRoutes = express.Router();
// Public authentication endpoints (with stricter rate limiting)
authRoutes.post('/register',
rateLimiters.auth,
validators.validatePlayerRegistration,
authController.register
);
authRoutes.post('/login',
rateLimiters.auth,
validators.validatePlayerLogin,
authController.login
);
// Protected authentication endpoints
authRoutes.post('/logout',
authenticatePlayer,
authController.logout
);
authRoutes.post('/refresh',
rateLimiters.auth,
authController.refresh
);
authRoutes.get('/me',
authenticatePlayer,
authController.getProfile
);
authRoutes.put('/me',
authenticatePlayer,
validateRequest(require('joi').object({
username: require('joi').string().alphanum().min(3).max(20).optional()
}), 'body'),
authController.updateProfile
);
authRoutes.get('/verify',
authenticatePlayer,
authController.verifyToken
);
authRoutes.post('/change-password',
authenticatePlayer,
rateLimiters.auth,
validateRequest(require('joi').object({
currentPassword: require('joi').string().required(),
newPassword: require('joi').string().min(8).max(128).required()
}), 'body'),
authController.changePassword
);
// Mount authentication routes
router.use('/auth', authRoutes);
/**
* Player Management Routes
* /api/player/*
*/
const playerRoutes = express.Router();
// All player routes require authentication
playerRoutes.use(authenticatePlayer);
playerRoutes.get('/dashboard', playerController.getDashboard);
playerRoutes.get('/resources', playerController.getResources);
playerRoutes.get('/stats', playerController.getStats);
playerRoutes.put('/settings',
validateRequest(require('joi').object({
// TODO: Define settings schema
notifications: require('joi').object({
email: require('joi').boolean().optional(),
push: require('joi').boolean().optional(),
battles: require('joi').boolean().optional(),
colonies: require('joi').boolean().optional()
}).optional(),
ui: require('joi').object({
theme: require('joi').string().valid('light', 'dark').optional(),
language: require('joi').string().valid('en', 'es', 'fr', 'de').optional()
}).optional()
}), 'body'),
playerController.updateSettings
);
playerRoutes.get('/activity',
validators.validatePagination,
playerController.getActivity
);
playerRoutes.get('/notifications',
validateRequest(require('joi').object({
unreadOnly: require('joi').boolean().default(false)
}), 'query'),
playerController.getNotifications
);
playerRoutes.put('/notifications/read',
validateRequest(require('joi').object({
notificationIds: require('joi').array().items(
require('joi').number().integer().positive()
).min(1).required()
}), 'body'),
playerController.markNotificationsRead
);
// Mount player routes
router.use('/player', playerRoutes);
/**
* Game Feature Routes
* These will be expanded with actual game functionality
*/
// Colonies Routes (placeholder)
router.get('/colonies',
authenticatePlayer,
validators.validatePagination,
(req, res) => {
res.json({
success: true,
message: 'Colonies endpoint - feature not yet implemented',
data: {
colonies: [],
pagination: {
page: 1,
limit: 20,
total: 0,
totalPages: 0
}
},
correlationId: req.correlationId
});
}
);
router.post('/colonies',
authenticatePlayer,
rateLimiters.gameAction,
validators.validateColonyCreation,
(req, res) => {
res.status(501).json({
success: false,
message: 'Colony creation feature not yet implemented',
correlationId: req.correlationId
});
}
);
// Fleets Routes (placeholder)
router.get('/fleets',
authenticatePlayer,
validators.validatePagination,
(req, res) => {
res.json({
success: true,
message: 'Fleets endpoint - feature not yet implemented',
data: {
fleets: [],
pagination: {
page: 1,
limit: 20,
total: 0,
totalPages: 0
}
},
correlationId: req.correlationId
});
}
);
router.post('/fleets',
authenticatePlayer,
rateLimiters.gameAction,
validators.validateFleetCreation,
(req, res) => {
res.status(501).json({
success: false,
message: 'Fleet creation feature not yet implemented',
correlationId: req.correlationId
});
}
);
// Research Routes (placeholder)
router.get('/research',
authenticatePlayer,
(req, res) => {
res.json({
success: true,
message: 'Research endpoint - feature not yet implemented',
data: {
currentResearch: null,
availableResearch: [],
completedResearch: []
},
correlationId: req.correlationId
});
}
);
router.post('/research',
authenticatePlayer,
rateLimiters.gameAction,
validators.validateResearchInitiation,
(req, res) => {
res.status(501).json({
success: false,
message: 'Research initiation feature not yet implemented',
correlationId: req.correlationId
});
}
);
// Galaxy Routes (placeholder)
router.get('/galaxy',
authenticatePlayer,
validateRequest(require('joi').object({
sector: require('joi').string().pattern(/^[A-Z]\d+$/).optional(),
coordinates: require('joi').string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).optional()
}), 'query'),
(req, res) => {
const { sector, coordinates } = req.query;
res.json({
success: true,
message: 'Galaxy endpoint - feature not yet implemented',
data: {
sector: sector || null,
coordinates: coordinates || null,
systems: [],
playerColonies: [],
playerFleets: []
},
correlationId: req.correlationId
});
}
);
// Messages Routes (placeholder)
router.get('/messages',
authenticatePlayer,
validators.validatePagination,
(req, res) => {
res.json({
success: true,
message: 'Messages endpoint - feature not yet implemented',
data: {
messages: [],
unreadCount: 0,
pagination: {
page: 1,
limit: 20,
total: 0,
totalPages: 0
}
},
correlationId: req.correlationId
});
}
);
router.post('/messages',
authenticatePlayer,
rateLimiters.messaging,
validators.validateMessageSend,
(req, res) => {
res.status(501).json({
success: false,
message: 'Message sending feature not yet implemented',
correlationId: req.correlationId
});
}
);
/**
* Error handling for API routes
*/
router.use('*', (req, res) => {
res.status(404).json({
success: false,
error: 'API endpoint not found',
message: `The endpoint ${req.method} ${req.originalUrl} does not exist`,
correlationId: req.correlationId,
timestamp: new Date().toISOString()
});
});
module.exports = router;

314
src/routes/debug.js Normal file
View file

@ -0,0 +1,314 @@
/**
* Debug Routes (Development Only)
* Provides debugging endpoints for development and testing
*/
const express = require('express');
const router = express.Router();
const db = require('../database/connection');
const { getRedisClient } = require('../config/redis');
const { getWebSocketServer, getConnectionStats } = require('../config/websocket');
const logger = require('../utils/logger');
// Middleware to ensure debug routes are only available in development
router.use((req, res, next) => {
if (process.env.NODE_ENV !== 'development') {
return res.status(404).json({
error: 'Debug endpoints not available in production'
});
}
next();
});
/**
* Debug API Information
*/
router.get('/', (req, res) => {
res.json({
name: 'Shattered Void - Debug API',
environment: process.env.NODE_ENV,
timestamp: new Date().toISOString(),
correlationId: req.correlationId,
endpoints: {
database: '/debug/database',
redis: '/debug/redis',
websocket: '/debug/websocket',
system: '/debug/system',
logs: '/debug/logs',
player: '/debug/player/:playerId'
}
});
});
/**
* Database Debug Information
*/
router.get('/database', async (req, res) => {
try {
// Test database connection
const dbTest = await db.raw('SELECT NOW() as current_time, version() as db_version');
// Get table information
const tables = await db.raw(`
SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema = ?
AND table_type = 'BASE TABLE'
`, [process.env.DB_NAME || 'shattered_void_dev']);
res.json({
status: 'connected',
connection: {
host: process.env.DB_HOST,
database: process.env.DB_NAME,
currentTime: dbTest.rows[0].current_time,
version: dbTest.rows[0].db_version
},
tables: tables.rows,
correlationId: req.correlationId
});
} catch (error) {
logger.error('Database debug error:', error);
res.status(500).json({
status: 'error',
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* Redis Debug Information
*/
router.get('/redis', async (req, res) => {
try {
const redisClient = getRedisClient();
if (!redisClient) {
return res.json({
status: 'not_connected',
message: 'Redis client not available',
correlationId: req.correlationId
});
}
// Test Redis connection
const pong = await redisClient.ping();
const info = await redisClient.info();
res.json({
status: 'connected',
ping: pong,
info: info.split('\r\n').slice(0, 20), // First 20 lines of info
correlationId: req.correlationId
});
} catch (error) {
logger.error('Redis debug error:', error);
res.status(500).json({
status: 'error',
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* WebSocket Debug Information
*/
router.get('/websocket', (req, res) => {
try {
const io = getWebSocketServer();
const stats = getConnectionStats();
if (!io) {
return res.json({
status: 'not_initialized',
message: 'WebSocket server not available',
correlationId: req.correlationId
});
}
res.json({
status: 'running',
stats,
sockets: {
count: io.sockets.sockets.size,
rooms: Array.from(io.sockets.adapter.rooms.keys())
},
correlationId: req.correlationId
});
} catch (error) {
logger.error('WebSocket debug error:', error);
res.status(500).json({
status: 'error',
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* System Debug Information
*/
router.get('/system', (req, res) => {
const memUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
res.json({
process: {
pid: process.pid,
uptime: process.uptime(),
version: process.version,
platform: process.platform,
arch: process.arch
},
memory: {
rss: Math.round(memUsage.rss / 1024 / 1024),
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
external: Math.round(memUsage.external / 1024 / 1024)
},
cpu: {
user: cpuUsage.user,
system: cpuUsage.system
},
environment: {
nodeEnv: process.env.NODE_ENV,
port: process.env.PORT,
logLevel: process.env.LOG_LEVEL
},
correlationId: req.correlationId
});
});
/**
* Recent Logs Debug Information
*/
router.get('/logs', (req, res) => {
const { level = 'info', limit = 50 } = req.query;
// Note: This is a placeholder. In a real implementation,
// you'd want to read from your log files or log storage system
res.json({
message: 'Log retrieval not implemented',
note: 'This would show recent log entries filtered by level',
requested: {
level,
limit: parseInt(limit)
},
suggestion: 'Check log files directly in logs/ directory',
correlationId: req.correlationId
});
});
/**
* Player Debug Information
*/
router.get('/player/:playerId', async (req, res) => {
try {
const playerId = parseInt(req.params.playerId);
if (isNaN(playerId)) {
return res.status(400).json({
error: 'Invalid player ID',
correlationId: req.correlationId
});
}
// Get comprehensive player information
const player = await db('players')
.where('id', playerId)
.first();
if (!player) {
return res.status(404).json({
error: 'Player not found',
correlationId: req.correlationId
});
}
const resources = await db('player_resources')
.where('player_id', playerId)
.first();
const stats = await db('player_stats')
.where('player_id', playerId)
.first();
const colonies = await db('colonies')
.where('player_id', playerId)
.select(['id', 'name', 'coordinates', 'created_at']);
const fleets = await db('fleets')
.where('player_id', playerId)
.select(['id', 'name', 'status', 'created_at']);
// Remove sensitive information
delete player.password_hash;
res.json({
player,
resources,
stats,
colonies,
fleets,
summary: {
totalColonies: colonies.length,
totalFleets: fleets.length,
accountAge: Math.floor((Date.now() - new Date(player.created_at).getTime()) / (1000 * 60 * 60 * 24))
},
correlationId: req.correlationId
});
} catch (error) {
logger.error('Player debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
}
});
/**
* Test Endpoint for Various Scenarios
*/
router.get('/test/:scenario', (req, res) => {
const { scenario } = req.params;
switch (scenario) {
case 'error':
throw new Error('Test error for debugging');
case 'slow':
setTimeout(() => {
res.json({
message: 'Slow response test completed',
delay: '3 seconds',
correlationId: req.correlationId
});
}, 3000);
break;
case 'memory':
// Create a large object to test memory usage
const largeArray = new Array(1000000).fill('test data');
res.json({
message: 'Memory test completed',
arrayLength: largeArray.length,
correlationId: req.correlationId
});
break;
default:
res.json({
message: 'Test scenario not recognized',
availableScenarios: ['error', 'slow', 'memory'],
correlationId: req.correlationId
});
}
});
module.exports = router;

144
src/routes/index.js Normal file
View file

@ -0,0 +1,144 @@
/**
* Main Routes Index
* Central routing configuration that imports and organizes all route modules
*/
const express = require('express');
const logger = require('../utils/logger');
const router = express.Router();
// Import route modules
const apiRoutes = require('./api');
const adminRoutes = require('./admin');
/**
* Root endpoint - API information
*/
router.get('/', (req, res) => {
const apiInfo = {
name: 'Shattered Void MMO API',
version: process.env.npm_package_version || '0.1.0',
environment: process.env.NODE_ENV || 'development',
status: 'operational',
timestamp: new Date().toISOString(),
endpoints: {
health: '/health',
api: '/api',
admin: '/api/admin'
},
documentation: {
api: '/docs/api',
admin: '/docs/admin'
},
correlationId: req.correlationId
};
res.json(apiInfo);
});
/**
* API Documentation endpoint (placeholder)
*/
router.get('/docs', (req, res) => {
res.json({
message: 'API Documentation',
note: 'Interactive API documentation will be available here',
version: process.env.npm_package_version || '0.1.0',
correlationId: req.correlationId,
links: {
playerAPI: '/docs/api',
adminAPI: '/docs/admin'
}
});
});
/**
* Player API Documentation (placeholder)
*/
router.get('/docs/api', (req, res) => {
res.json({
title: 'Shattered Void - Player API Documentation',
version: process.env.npm_package_version || '0.1.0',
description: 'API endpoints for player operations',
baseUrl: '/api',
correlationId: req.correlationId,
endpoints: {
authentication: {
register: 'POST /api/auth/register',
login: 'POST /api/auth/login',
logout: 'POST /api/auth/logout',
profile: 'GET /api/auth/me',
updateProfile: 'PUT /api/auth/me',
verify: 'GET /api/auth/verify'
},
player: {
dashboard: 'GET /api/player/dashboard',
resources: 'GET /api/player/resources',
stats: 'GET /api/player/stats',
notifications: 'GET /api/player/notifications'
},
game: {
colonies: 'GET /api/colonies',
fleets: 'GET /api/fleets',
research: 'GET /api/research',
galaxy: 'GET /api/galaxy'
}
},
note: 'Full interactive documentation coming soon'
});
});
/**
* Admin API Documentation (placeholder)
*/
router.get('/docs/admin', (req, res) => {
res.json({
title: 'Shattered Void - Admin API Documentation',
version: process.env.npm_package_version || '0.1.0',
description: 'API endpoints for administrative operations',
baseUrl: '/api/admin',
correlationId: req.correlationId,
endpoints: {
authentication: {
login: 'POST /api/admin/auth/login',
logout: 'POST /api/admin/auth/logout',
profile: 'GET /api/admin/auth/me',
verify: 'GET /api/admin/auth/verify',
stats: 'GET /api/admin/auth/stats'
},
playerManagement: {
listPlayers: 'GET /api/admin/players',
getPlayer: 'GET /api/admin/players/:id',
updatePlayer: 'PUT /api/admin/players/:id',
deactivatePlayer: 'DELETE /api/admin/players/:id'
},
systemManagement: {
systemStats: 'GET /api/admin/system/stats',
events: 'GET /api/admin/events',
analytics: 'GET /api/admin/analytics'
}
},
note: 'Full interactive documentation coming soon'
});
});
// Mount route modules
router.use('/api', apiRoutes);
// Admin routes (if enabled)
if (process.env.ENABLE_ADMIN_ROUTES !== 'false') {
router.use('/api/admin', adminRoutes);
logger.info('Admin routes enabled');
} else {
logger.info('Admin routes disabled');
}
// Debug routes (development only)
if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEBUG_ENDPOINTS === 'true') {
const debugRoutes = require('./debug');
router.use('/debug', debugRoutes);
logger.info('Debug routes enabled');
}
module.exports = router;

67
src/routes/player/auth.js Normal file
View file

@ -0,0 +1,67 @@
/**
* Player authentication routes
*/
const express = require('express');
const { asyncHandler } = require('../../middleware/error-handler');
const router = express.Router();
// Import authentication service
// const authService = require('../../services/auth.service');
// Player registration
router.post('/register', asyncHandler(async (req, res) => {
// TODO: Implement player registration
res.status(501).json({
error: 'Not implemented yet',
message: 'Player registration endpoint needs implementation',
});
}));
// Player login
router.post('/login', asyncHandler(async (req, res) => {
// TODO: Implement player login
res.status(501).json({
error: 'Not implemented yet',
message: 'Player login endpoint needs implementation',
});
}));
// Player logout
router.post('/logout', asyncHandler(async (req, res) => {
// TODO: Implement player logout
res.status(501).json({
error: 'Not implemented yet',
message: 'Player logout endpoint needs implementation',
});
}));
// Email verification
router.post('/verify-email', asyncHandler(async (req, res) => {
// TODO: Implement email verification
res.status(501).json({
error: 'Not implemented yet',
message: 'Email verification endpoint needs implementation',
});
}));
// Password reset request
router.post('/forgot-password', asyncHandler(async (req, res) => {
// TODO: Implement password reset request
res.status(501).json({
error: 'Not implemented yet',
message: 'Password reset endpoint needs implementation',
});
}));
// Password reset
router.post('/reset-password', asyncHandler(async (req, res) => {
// TODO: Implement password reset
res.status(501).json({
error: 'Not implemented yet',
message: 'Password reset endpoint needs implementation',
});
}));
module.exports = router;

View file

View file

View file

View file

View file

@ -0,0 +1,48 @@
/**
* Player API routes
*/
const express = require('express');
const { authenticateToken, optionalAuth } = require('../../middleware/auth');
const { asyncHandler } = require('../../middleware/error-handler');
const router = express.Router();
// Import route modules
const authRoutes = require('./auth');
const profileRoutes = require('./profile');
const coloniesRoutes = require('./colonies');
const fleetsRoutes = require('./fleets');
const researchRoutes = require('./research');
const galaxyRoutes = require('./galaxy');
const eventsRoutes = require('./events');
const notificationsRoutes = require('./notifications');
// Public routes (no authentication required)
router.use('/auth', authRoutes);
router.use('/galaxy', optionalAuth('player'), galaxyRoutes);
// Protected routes (authentication required)
router.use('/profile', authenticateToken('player'), profileRoutes);
router.use('/colonies', authenticateToken('player'), coloniesRoutes);
router.use('/fleets', authenticateToken('player'), fleetsRoutes);
router.use('/research', authenticateToken('player'), researchRoutes);
router.use('/events', authenticateToken('player'), eventsRoutes);
router.use('/notifications', authenticateToken('player'), notificationsRoutes);
// Player status endpoint
router.get('/status', authenticateToken('player'), asyncHandler(async (req, res) => {
res.json({
status: 'authenticated',
player: {
id: req.user.id,
username: req.user.username,
displayName: req.user.display_name,
userGroup: req.user.user_group,
lastActive: req.user.last_active_at,
},
timestamp: new Date().toISOString(),
});
}));
module.exports = router;

View file

View file

View file

201
src/server.js Normal file
View file

@ -0,0 +1,201 @@
/**
* Shattered Void MMO - Main Server Entry Point
* Initializes Express server with all middleware, routes, and WebSocket connections
*/
const http = require('http');
require('dotenv').config();
// Import core modules
const createApp = require('./app');
const logger = require('./utils/logger');
const { initializeDatabase } = require('./database/connection');
const { initializeRedis } = require('./config/redis');
const { initializeWebSocket } = require('./config/websocket');
const { initializeGameTick } = require('./services/game-tick.service');
// Configuration
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
// Global instances
let app;
let server;
let io;
/**
* Initialize all core systems
*/
async function initializeSystems() {
try {
logger.info('Initializing core systems...');
// Initialize database connections
if (process.env.DISABLE_DATABASE !== 'true') {
await initializeDatabase();
logger.info('Database systems initialized');
} else {
logger.warn('Database disabled by environment variable');
}
// Initialize Redis
if (process.env.DISABLE_REDIS !== 'true') {
await initializeRedis();
logger.info('Redis systems initialized');
} else {
logger.warn('Redis disabled by environment variable');
}
// Initialize WebSocket
io = await initializeWebSocket(server);
logger.info('WebSocket systems initialized');
// Initialize game systems
await initializeGameSystems();
logger.info('Game systems initialized');
} catch (error) {
logger.error('Failed to initialize systems:', error);
throw error;
}
}
/**
* Initialize game systems (tick processing, etc.)
*/
async function initializeGameSystems() {
try {
// Initialize game tick system
if (process.env.ENABLE_GAME_TICK !== 'false') {
await initializeGameTick();
logger.info('Game tick system initialized');
}
// Add other game system initializations here
} catch (error) {
logger.error('Game systems initialization failed:', error);
throw error;
}
}
/**
* Graceful shutdown handler
*/
function setupGracefulShutdown() {
const shutdown = async (signal) => {
logger.info(`Received ${signal}. Starting graceful shutdown...`);
try {
// Stop accepting new connections
if (server) {
server.close(() => {
logger.info('HTTP server closed');
});
}
// Close WebSocket connections
if (io) {
io.close(() => {
logger.info('WebSocket server closed');
});
}
// Close database connections
const db = require('./database/connection');
if (db) {
await db.destroy();
logger.info('Database connections closed');
}
// Close Redis connection
const redisConfig = require('./config/redis');
if (redisConfig.client) {
await redisConfig.client.quit();
logger.info('Redis connection closed');
}
logger.info('Graceful shutdown completed');
process.exit(0);
} catch (error) {
logger.error('Error during shutdown:', error);
process.exit(1);
}
};
// Handle shutdown signals
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Promise Rejection:', {
reason: reason?.message || reason,
stack: reason?.stack,
promise: promise?.toString()
});
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', {
message: error.message,
stack: error.stack
});
process.exit(1);
});
}
/**
* Start the application server
*/
async function startServer() {
try {
logger.info(`Starting Shattered Void MMO Server in ${NODE_ENV} mode...`);
// Create Express app
app = createApp();
// Create HTTP server
server = http.createServer(app);
// Set up graceful shutdown handlers
setupGracefulShutdown();
// Initialize all systems
await initializeSystems();
// Start the server
server.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${NODE_ENV}`);
logger.info(`Process ID: ${process.pid}`);
// Log memory usage
const memUsage = process.memoryUsage();
logger.info('Initial memory usage:', {
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
});
logger.info('Shattered Void MMO Server started successfully');
});
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
// Start the server if this file is run directly
if (require.main === module) {
startServer();
}
module.exports = {
startServer,
getApp: () => app,
getServer: () => server,
getIO: () => io
};

View file

@ -0,0 +1,412 @@
/**
* Game tick processing service
* Handles the core game loop for resource production, fleet movements, etc.
*/
const cron = require('node-cron');
const logger = require('../utils/logger');
const db = require('../database/connection');
const redisClient = require('../utils/redis');
class GameTickService {
constructor() {
this.isInitialized = false;
this.currentTick = 0;
this.cronJob = null;
this.config = null;
}
/**
* Initialize the game tick system
*/
async initialize() {
try {
// Load configuration
await this.loadConfig();
// Get current tick number
await this.loadCurrentTick();
// Start the cron job
this.startTickScheduler();
this.isInitialized = true;
logger.info('Game tick service initialized', {
tickInterval: this.config.tick_interval_ms,
maxUserGroups: this.config.max_user_groups,
currentTick: this.currentTick,
});
} catch (error) {
logger.error('Failed to initialize game tick service:', error);
throw error;
}
}
/**
* Load configuration from database
*/
async loadConfig() {
const config = await db('game_tick_config')
.where('is_active', true)
.first();
if (!config) {
throw new Error('No active game tick configuration found');
}
this.config = config;
}
/**
* Load current tick number from logs
*/
async loadCurrentTick() {
const lastTick = await db('game_tick_log')
.select('tick_number')
.orderBy('tick_number', 'desc')
.first();
this.currentTick = lastTick ? lastTick.tick_number : 0;
}
/**
* Start the cron job scheduler
*/
startTickScheduler() {
const intervalMs = this.config.tick_interval_ms;
const cronPattern = this.createCronPattern(intervalMs);
this.cronJob = cron.schedule(cronPattern, async () => {
await this.processTick();
}, {
scheduled: true,
timezone: 'UTC',
});
logger.info('Game tick scheduler started', {
pattern: cronPattern,
intervalMs,
});
}
/**
* Create cron pattern from milliseconds
* @param {number} intervalMs - Interval in milliseconds
* @returns {string} Cron pattern
*/
createCronPattern(intervalMs) {
const intervalMinutes = Math.floor(intervalMs / 60000);
if (intervalMinutes === 1) {
return '* * * * *'; // Every minute
} else if (intervalMinutes === 5) {
return '*/5 * * * *'; // Every 5 minutes
} else if (intervalMinutes === 10) {
return '*/10 * * * *'; // Every 10 minutes
} else if (intervalMinutes === 30) {
return '*/30 * * * *'; // Every 30 minutes
} else if (intervalMinutes === 60) {
return '0 * * * *'; // Every hour
} else {
// Default to every minute for non-standard intervals
return '* * * * *';
}
}
/**
* Process a single game tick
*/
async processTick() {
const tickNumber = ++this.currentTick;
const startTime = new Date();
logger.info('Starting game tick', { tickNumber });
// Process each user group
for (let userGroup = 1; userGroup <= this.config.max_user_groups; userGroup++) {
await this.processUserGroupTick(tickNumber, userGroup);
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
logger.info('Game tick completed', {
tickNumber,
duration: `${duration}ms`,
});
}
/**
* Process tick for a specific user group
* @param {number} tickNumber - Current tick number
* @param {number} userGroup - User group to process
*/
async processUserGroupTick(tickNumber, userGroup) {
const startTime = new Date();
let processedPlayers = 0;
let attempt = 0;
while (attempt < this.config.max_retry_attempts) {
try {
// Log tick start
const logId = await this.logTickStart(tickNumber, userGroup, attempt);
// Get players in this user group
const players = await db('players')
.where('user_group', userGroup)
.where('account_status', 'active')
.select('id');
// Process each player
for (const player of players) {
await this.processPlayerTick(tickNumber, player.id);
processedPlayers++;
}
// Log successful completion
await this.logTickComplete(logId, processedPlayers);
logger.debug('User group tick completed', {
tickNumber,
userGroup,
processedPlayers,
attempt: attempt + 1,
});
break; // Success, exit retry loop
} catch (error) {
attempt++;
logger.error('User group tick failed', {
tickNumber,
userGroup,
attempt,
error: error.message,
});
if (attempt >= this.config.max_retry_attempts) {
// Max retries reached, log failure
await this.logTickFailure(tickNumber, userGroup, attempt, error.message);
// Apply bonus tick if configured
if (attempt >= this.config.bonus_tick_threshold) {
await this.applyBonusTick(tickNumber, userGroup);
}
} else {
// Wait before retry
await this.sleep(this.config.retry_delay_ms);
}
}
}
}
/**
* Process tick for a single player
* @param {number} tickNumber - Current tick number
* @param {number} playerId - Player ID
*/
async processPlayerTick(tickNumber, playerId) {
try {
// Use lock to prevent concurrent processing
const lockKey = `player_tick:${playerId}`;
const lockToken = await redisClient.lock.acquire(lockKey, 30);
if (!lockToken) {
logger.warn('Could not acquire player tick lock', { playerId, tickNumber });
return;
}
try {
// Process resource production
await this.processResourceProduction(playerId, tickNumber);
// Process building construction
await this.processBuildingConstruction(playerId, tickNumber);
// Process research
await this.processResearch(playerId, tickNumber);
// Process fleet movements
await this.processFleetMovements(playerId, tickNumber);
// Update player last tick processed
await db('players')
.where('id', playerId)
.update({ last_tick_processed: tickNumber });
} finally {
await redisClient.lock.release(lockKey, lockToken);
}
} catch (error) {
logger.error('Player tick processing failed', {
playerId,
tickNumber,
error: error.message,
});
throw error;
}
}
/**
* Process resource production for player colonies
* @param {number} playerId - Player ID
* @param {number} tickNumber - Current tick number
*/
async processResourceProduction(playerId, tickNumber) {
// TODO: Implement resource production logic
logger.debug('Processing resource production', { playerId, tickNumber });
}
/**
* Process building construction
* @param {number} playerId - Player ID
* @param {number} tickNumber - Current tick number
*/
async processBuildingConstruction(playerId, tickNumber) {
// TODO: Implement building construction logic
logger.debug('Processing building construction', { playerId, tickNumber });
}
/**
* Process research progress
* @param {number} playerId - Player ID
* @param {number} tickNumber - Current tick number
*/
async processResearch(playerId, tickNumber) {
// TODO: Implement research progress logic
logger.debug('Processing research', { playerId, tickNumber });
}
/**
* Process fleet movements
* @param {number} playerId - Player ID
* @param {number} tickNumber - Current tick number
*/
async processFleetMovements(playerId, tickNumber) {
// TODO: Implement fleet movement logic
logger.debug('Processing fleet movements', { playerId, tickNumber });
}
/**
* Log tick start
* @param {number} tickNumber - Tick number
* @param {number} userGroup - User group
* @param {number} attempt - Attempt number
* @returns {Promise<number>} Log entry ID
*/
async logTickStart(tickNumber, userGroup, attempt) {
const [logEntry] = await db('game_tick_log')
.insert({
tick_number: tickNumber,
user_group: userGroup,
started_at: new Date(),
status: 'running',
retry_attempt: attempt,
})
.returning('id');
return logEntry.id;
}
/**
* Log tick completion
* @param {number} logId - Log entry ID
* @param {number} processedPlayers - Number of players processed
*/
async logTickComplete(logId, processedPlayers) {
const completedAt = new Date();
await db('game_tick_log')
.where('id', logId)
.update({
completed_at: completedAt,
status: 'completed',
processed_players: processedPlayers,
execution_time_ms: completedAt.getTime() - new Date().getTime(),
});
}
/**
* Log tick failure
* @param {number} tickNumber - Tick number
* @param {number} userGroup - User group
* @param {number} attempt - Final attempt number
* @param {string} errorMessage - Error message
*/
async logTickFailure(tickNumber, userGroup, attempt, errorMessage) {
await db('game_tick_log')
.insert({
tick_number: tickNumber,
user_group: userGroup,
started_at: new Date(),
completed_at: new Date(),
status: 'failed',
retry_attempt: attempt,
error_message: errorMessage,
});
}
/**
* Apply bonus tick for failed processing
* @param {number} tickNumber - Tick number
* @param {number} userGroup - User group
*/
async applyBonusTick(tickNumber, userGroup) {
logger.info('Applying bonus tick', { tickNumber, userGroup });
await db('game_tick_log')
.where('tick_number', tickNumber)
.where('user_group', userGroup)
.update({ bonus_tick_applied: true });
// TODO: Implement actual bonus tick logic
}
/**
* Utility sleep function
* @param {number} ms - Milliseconds to sleep
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Stop the game tick service
*/
stop() {
if (this.cronJob) {
this.cronJob.stop();
this.cronJob = null;
logger.info('Game tick service stopped');
}
}
/**
* Get service status
* @returns {Object} Service status
*/
getStatus() {
return {
initialized: this.isInitialized,
currentTick: this.currentTick,
running: this.cronJob ? this.cronJob.running : false,
config: this.config,
};
}
}
// Create singleton instance
const gameTickService = new GameTickService();
/**
* Initialize game tick service
*/
async function initializeGameTick() {
await gameTickService.initialize();
}
module.exports = {
gameTickService,
initializeGameTick,
};

View file

@ -0,0 +1,570 @@
/**
* Admin Service
* Handles all admin-related business logic including authentication, user management, and system operations
*/
const db = require('../../database/connection');
const { hashPassword, verifyPassword } = require('../../utils/password');
const { generateAdminToken, generateRefreshToken } = require('../../utils/jwt');
const { validateEmail } = require('../../utils/validation');
const logger = require('../../utils/logger');
const { ValidationError, ConflictError, NotFoundError, AuthenticationError, AuthorizationError } = require('../../middleware/error.middleware');
class AdminService {
/**
* Authenticate admin login
* @param {Object} loginData - Login credentials
* @param {string} loginData.email - Admin email
* @param {string} loginData.password - Admin password
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Authentication result with tokens
*/
async authenticateAdmin(loginData, correlationId) {
try {
const { email, password } = loginData;
logger.info('Admin authentication initiated', {
correlationId,
email
});
// Find admin by email
const admin = await this.findAdminByEmail(email);
if (!admin) {
throw new AuthenticationError('Invalid email or password');
}
// Check if admin is active
if (!admin.is_active) {
throw new AuthenticationError('Account has been deactivated');
}
// Verify password
const isPasswordValid = await verifyPassword(password, admin.password_hash);
if (!isPasswordValid) {
logger.warn('Admin authentication failed - invalid password', {
correlationId,
adminId: admin.id,
email: admin.email
});
throw new AuthenticationError('Invalid email or password');
}
// Get admin permissions
const permissions = await this.getAdminPermissions(admin.id);
// Generate tokens
const accessToken = generateAdminToken({
adminId: admin.id,
email: admin.email,
username: admin.username,
permissions: permissions
});
const refreshToken = generateRefreshToken({
userId: admin.id,
type: 'admin'
});
// Update last login timestamp
await db('admins')
.where('id', admin.id)
.update({
last_login_at: new Date(),
updated_at: new Date()
});
logger.audit('Admin authenticated successfully', {
correlationId,
adminId: admin.id,
email: admin.email,
username: admin.username,
permissions: permissions
});
return {
admin: {
id: admin.id,
email: admin.email,
username: admin.username,
permissions: permissions,
isActive: admin.is_active
},
tokens: {
accessToken,
refreshToken
}
};
} catch (error) {
logger.error('Admin authentication failed', {
correlationId,
email: loginData.email,
error: error.message
});
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError('Authentication failed');
}
}
/**
* Get admin profile by ID
* @param {number} adminId - Admin ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Admin profile data
*/
async getAdminProfile(adminId, correlationId) {
try {
logger.info('Fetching admin profile', {
correlationId,
adminId
});
const admin = await db('admins')
.select([
'id',
'email',
'username',
'is_active',
'created_at',
'last_login_at'
])
.where('id', adminId)
.first();
if (!admin) {
throw new NotFoundError('Admin not found');
}
// Get admin permissions
const permissions = await this.getAdminPermissions(adminId);
const profile = {
id: admin.id,
email: admin.email,
username: admin.username,
permissions: permissions,
isActive: admin.is_active,
createdAt: admin.created_at,
lastLoginAt: admin.last_login_at
};
logger.info('Admin profile retrieved successfully', {
correlationId,
adminId,
username: admin.username
});
return profile;
} catch (error) {
logger.error('Failed to fetch admin profile', {
correlationId,
adminId,
error: error.message,
stack: error.stack
});
if (error instanceof NotFoundError) {
throw error;
}
throw new Error('Failed to retrieve admin profile');
}
}
/**
* Get players list with pagination and filtering
* @param {Object} options - Query options
* @param {number} options.page - Page number
* @param {number} options.limit - Items per page
* @param {string} options.sortBy - Sort field
* @param {string} options.sortOrder - Sort order (asc/desc)
* @param {string} options.search - Search term
* @param {boolean} options.activeOnly - Filter active players only
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Players list with pagination info
*/
async getPlayersList(options, correlationId) {
try {
const {
page = 1,
limit = 20,
sortBy = 'created_at',
sortOrder = 'desc',
search = '',
activeOnly = null
} = options;
logger.info('Fetching players list', {
correlationId,
page,
limit,
sortBy,
sortOrder,
search,
activeOnly
});
let query = db('players')
.select([
'id',
'email',
'username',
'is_active',
'is_verified',
'created_at',
'last_login_at'
]);
// Apply search filter
if (search) {
query = query.where(function() {
this.whereILike('username', `%${search}%`)
.orWhereILike('email', `%${search}%`);
});
}
// Apply active filter
if (activeOnly !== null) {
query = query.where('is_active', activeOnly);
}
// Get total count
const totalQuery = query.clone();
const totalCount = await totalQuery.count('* as count').first();
const total = parseInt(totalCount.count);
// Apply pagination and sorting
const offset = (page - 1) * limit;
const players = await query
.orderBy(sortBy, sortOrder)
.limit(limit)
.offset(offset);
const result = {
players,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
};
logger.info('Players list retrieved successfully', {
correlationId,
playersCount: players.length,
total,
page
});
return result;
} catch (error) {
logger.error('Failed to fetch players list', {
correlationId,
error: error.message,
stack: error.stack
});
throw new Error('Failed to retrieve players list');
}
}
/**
* Get detailed player information for admin view
* @param {number} playerId - Player ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Detailed player information
*/
async getPlayerDetails(playerId, correlationId) {
try {
logger.info('Fetching player details for admin', {
correlationId,
playerId
});
// Get basic player info
const player = await db('players')
.select([
'id',
'email',
'username',
'is_active',
'is_verified',
'created_at',
'updated_at',
'last_login_at'
])
.where('id', playerId)
.first();
if (!player) {
throw new NotFoundError('Player not found');
}
// Get player resources
const resources = await db('player_resources')
.where('player_id', playerId)
.first();
// Get player stats
const stats = await db('player_stats')
.where('player_id', playerId)
.first();
// Get colonies count
const coloniesCount = await db('colonies')
.where('player_id', playerId)
.count('* as count')
.first();
// Get fleets count
const fleetsCount = await db('fleets')
.where('player_id', playerId)
.count('* as count')
.first();
const playerDetails = {
...player,
resources: resources || {
scrap: 0,
energy: 0,
research_points: 0
},
stats: stats || {
colonies_count: 0,
fleets_count: 0,
total_battles: 0,
battles_won: 0
},
currentCounts: {
colonies: parseInt(coloniesCount.count),
fleets: parseInt(fleetsCount.count)
}
};
logger.audit('Player details accessed by admin', {
correlationId,
playerId,
playerUsername: player.username
});
return playerDetails;
} catch (error) {
logger.error('Failed to fetch player details', {
correlationId,
playerId,
error: error.message,
stack: error.stack
});
if (error instanceof NotFoundError) {
throw error;
}
throw new Error('Failed to retrieve player details');
}
}
/**
* Update player status (activate/deactivate)
* @param {number} playerId - Player ID
* @param {boolean} isActive - New active status
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Updated player data
*/
async updatePlayerStatus(playerId, isActive, correlationId) {
try {
logger.info('Updating player status', {
correlationId,
playerId,
isActive
});
// Check if player exists
const player = await db('players')
.where('id', playerId)
.first();
if (!player) {
throw new NotFoundError('Player not found');
}
// Update player status
await db('players')
.where('id', playerId)
.update({
is_active: isActive,
updated_at: new Date()
});
const updatedPlayer = await db('players')
.select(['id', 'email', 'username', 'is_active', 'updated_at'])
.where('id', playerId)
.first();
logger.audit('Player status updated by admin', {
correlationId,
playerId,
playerUsername: player.username,
previousStatus: player.is_active,
newStatus: isActive
});
return updatedPlayer;
} catch (error) {
logger.error('Failed to update player status', {
correlationId,
playerId,
isActive,
error: error.message,
stack: error.stack
});
if (error instanceof NotFoundError) {
throw error;
}
throw new Error('Failed to update player status');
}
}
/**
* Get system statistics for admin dashboard
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} System statistics
*/
async getSystemStats(correlationId) {
try {
logger.info('Fetching system statistics', { correlationId });
// Get player counts
const playerStats = await db('players')
.select([
db.raw('COUNT(*) as total_players'),
db.raw('COUNT(CASE WHEN is_active = true THEN 1 END) as active_players'),
db.raw('COUNT(CASE WHEN is_verified = true THEN 1 END) as verified_players'),
db.raw('COUNT(CASE WHEN created_at >= NOW() - INTERVAL \'24 hours\' THEN 1 END) as new_players_24h')
])
.first();
// Get colony and fleet counts
const gameStats = await db.raw(`
SELECT
(SELECT COUNT(*) FROM colonies) as total_colonies,
(SELECT COUNT(*) FROM fleets) as total_fleets,
(SELECT COUNT(*) FROM research_queue) as active_research
`);
// Get recent activity (last 24 hours)
const recentActivity = await db('players')
.select([
db.raw('COUNT(CASE WHEN last_login_at >= NOW() - INTERVAL \'24 hours\' THEN 1 END) as active_24h'),
db.raw('COUNT(CASE WHEN last_login_at >= NOW() - INTERVAL \'7 days\' THEN 1 END) as active_7d')
])
.first();
const stats = {
players: {
total: parseInt(playerStats.total_players),
active: parseInt(playerStats.active_players),
verified: parseInt(playerStats.verified_players),
newToday: parseInt(playerStats.new_players_24h)
},
game: {
totalColonies: parseInt(gameStats.rows[0].total_colonies),
totalFleets: parseInt(gameStats.rows[0].total_fleets),
activeResearch: parseInt(gameStats.rows[0].active_research)
},
activity: {
active24h: parseInt(recentActivity.active_24h),
active7d: parseInt(recentActivity.active_7d)
},
timestamp: new Date().toISOString()
};
logger.info('System statistics retrieved', {
correlationId,
totalPlayers: stats.players.total,
activePlayers: stats.players.active
});
return stats;
} catch (error) {
logger.error('Failed to fetch system statistics', {
correlationId,
error: error.message,
stack: error.stack
});
throw new Error('Failed to retrieve system statistics');
}
}
/**
* Find admin by email
* @param {string} email - Admin email
* @returns {Promise<Object|null>} Admin data or null
*/
async findAdminByEmail(email) {
try {
return await db('admins')
.where('email', email.toLowerCase().trim())
.first();
} catch (error) {
logger.error('Failed to find admin by email', { error: error.message });
return null;
}
}
/**
* Get admin permissions
* @param {number} adminId - Admin ID
* @returns {Promise<Array>} Array of permission strings
*/
async getAdminPermissions(adminId) {
try {
const permissions = await db('admin_permissions as ap')
.join('permissions as p', 'ap.permission_id', 'p.id')
.select('p.name')
.where('ap.admin_id', adminId);
return permissions.map(p => p.name);
} catch (error) {
logger.error('Failed to fetch admin permissions', {
adminId,
error: error.message
});
return [];
}
}
/**
* Check if admin has specific permission
* @param {number} adminId - Admin ID
* @param {string} permission - Permission to check
* @returns {Promise<boolean>} True if admin has permission
*/
async hasPermission(adminId, permission) {
try {
const permissions = await this.getAdminPermissions(adminId);
return permissions.includes(permission) || permissions.includes('super_admin');
} catch (error) {
logger.error('Failed to check admin permission', {
adminId,
permission,
error: error.message
});
return false;
}
}
}
module.exports = AdminService;

View file

@ -0,0 +1,472 @@
/**
* Player Service
* Handles all player-related business logic including registration, authentication, and profile management
*/
const db = require('../../database/connection');
const { hashPassword, verifyPassword, validatePasswordStrength } = require('../../utils/password');
const { generatePlayerToken, generateRefreshToken } = require('../../utils/jwt');
const { validateEmail, validateUsername } = require('../../utils/validation');
const logger = require('../../utils/logger');
const { ValidationError, ConflictError, NotFoundError, AuthenticationError } = require('../../middleware/error.middleware');
class PlayerService {
/**
* Register a new player
* @param {Object} playerData - Player registration data
* @param {string} playerData.email - Player email
* @param {string} playerData.username - Player username
* @param {string} playerData.password - Player password
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Registered player data
*/
async registerPlayer(playerData, correlationId) {
try {
const { email, username, password } = playerData;
logger.info('Player registration initiated', {
correlationId,
email,
username
});
// Validate input data
await this.validateRegistrationData({ email, username, password });
// Check if email already exists
const existingEmail = await this.findPlayerByEmail(email);
if (existingEmail) {
throw new ConflictError('Email address is already registered');
}
// Check if username already exists
const existingUsername = await this.findPlayerByUsername(username);
if (existingUsername) {
throw new ConflictError('Username is already taken');
}
// Hash password
const hashedPassword = await hashPassword(password);
// Create player in database transaction
const player = await db.transaction(async (trx) => {
const [newPlayer] = await trx('players')
.insert({
email: email.toLowerCase().trim(),
username: username.trim(),
password_hash: hashedPassword,
is_active: true,
is_verified: false, // Email verification required
created_at: new Date(),
updated_at: new Date()
})
.returning(['id', 'email', 'username', 'is_active', 'is_verified', 'created_at']);
// Create initial player resources
await trx('player_resources').insert({
player_id: newPlayer.id,
scrap: parseInt(process.env.STARTING_RESOURCES_SCRAP) || 1000,
energy: parseInt(process.env.STARTING_RESOURCES_ENERGY) || 500,
research_points: 0,
created_at: new Date(),
updated_at: new Date()
});
// Create initial player stats
await trx('player_stats').insert({
player_id: newPlayer.id,
colonies_count: 0,
fleets_count: 0,
total_battles: 0,
battles_won: 0,
created_at: new Date(),
updated_at: new Date()
});
logger.info('Player registered successfully', {
correlationId,
playerId: newPlayer.id,
email: newPlayer.email,
username: newPlayer.username
});
return newPlayer;
});
// Return player data without sensitive information
return {
id: player.id,
email: player.email,
username: player.username,
isActive: player.is_active,
isVerified: player.is_verified,
createdAt: player.created_at
};
} catch (error) {
logger.error('Player registration failed', {
correlationId,
email: playerData.email,
username: playerData.username,
error: error.message,
stack: error.stack
});
if (error instanceof ValidationError || error instanceof ConflictError) {
throw error;
}
throw new Error('Player registration failed');
}
}
/**
* Authenticate player login
* @param {Object} loginData - Login credentials
* @param {string} loginData.email - Player email
* @param {string} loginData.password - Player password
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Authentication result with tokens
*/
async authenticatePlayer(loginData, correlationId) {
try {
const { email, password } = loginData;
logger.info('Player authentication initiated', {
correlationId,
email
});
// Find player by email
const player = await this.findPlayerByEmail(email);
if (!player) {
throw new AuthenticationError('Invalid email or password');
}
// Check if player is active
if (!player.is_active) {
throw new AuthenticationError('Account has been deactivated');
}
// Verify password
const isPasswordValid = await verifyPassword(password, player.password_hash);
if (!isPasswordValid) {
logger.warn('Player authentication failed - invalid password', {
correlationId,
playerId: player.id,
email: player.email
});
throw new AuthenticationError('Invalid email or password');
}
// Generate tokens
const accessToken = generatePlayerToken({
playerId: player.id,
email: player.email,
username: player.username
});
const refreshToken = generateRefreshToken({
userId: player.id,
type: 'player'
});
// Update last login timestamp
await db('players')
.where('id', player.id)
.update({
last_login_at: new Date(),
updated_at: new Date()
});
logger.info('Player authenticated successfully', {
correlationId,
playerId: player.id,
email: player.email,
username: player.username
});
return {
player: {
id: player.id,
email: player.email,
username: player.username,
isActive: player.is_active,
isVerified: player.is_verified
},
tokens: {
accessToken,
refreshToken
}
};
} catch (error) {
logger.error('Player authentication failed', {
correlationId,
email: loginData.email,
error: error.message
});
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError('Authentication failed');
}
}
/**
* Get player profile by ID
* @param {number} playerId - Player ID
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Player profile data
*/
async getPlayerProfile(playerId, correlationId) {
try {
logger.info('Fetching player profile', {
correlationId,
playerId
});
const player = await db('players')
.select([
'id',
'email',
'username',
'is_active',
'is_verified',
'created_at',
'last_login_at'
])
.where('id', playerId)
.first();
if (!player) {
throw new NotFoundError('Player not found');
}
// Get player resources
const resources = await db('player_resources')
.select(['scrap', 'energy', 'research_points'])
.where('player_id', playerId)
.first();
// Get player stats
const stats = await db('player_stats')
.select([
'colonies_count',
'fleets_count',
'total_battles',
'battles_won'
])
.where('player_id', playerId)
.first();
const profile = {
id: player.id,
email: player.email,
username: player.username,
isActive: player.is_active,
isVerified: player.is_verified,
createdAt: player.created_at,
lastLoginAt: player.last_login_at,
resources: resources || {
scrap: 0,
energy: 0,
researchPoints: 0
},
stats: stats || {
coloniesCount: 0,
fleetsCount: 0,
totalBattles: 0,
battlesWon: 0
}
};
logger.info('Player profile retrieved successfully', {
correlationId,
playerId,
username: player.username
});
return profile;
} catch (error) {
logger.error('Failed to fetch player profile', {
correlationId,
playerId,
error: error.message,
stack: error.stack
});
if (error instanceof NotFoundError) {
throw error;
}
throw new Error('Failed to retrieve player profile');
}
}
/**
* Update player profile
* @param {number} playerId - Player ID
* @param {Object} updateData - Data to update
* @param {string} correlationId - Request correlation ID
* @returns {Promise<Object>} Updated player profile
*/
async updatePlayerProfile(playerId, updateData, correlationId) {
try {
logger.info('Updating player profile', {
correlationId,
playerId,
updateFields: Object.keys(updateData)
});
// Validate player exists
const existingPlayer = await this.findPlayerById(playerId);
if (!existingPlayer) {
throw new NotFoundError('Player not found');
}
// Validate update data
const allowedFields = ['username'];
const sanitizedData = {};
for (const [key, value] of Object.entries(updateData)) {
if (allowedFields.includes(key)) {
sanitizedData[key] = value;
}
}
// Validate username if being updated
if (sanitizedData.username) {
const usernameValidation = validateUsername(sanitizedData.username);
if (!usernameValidation.isValid) {
throw new ValidationError(usernameValidation.error);
}
// Check if username is already taken by another player
const existingUsername = await db('players')
.where('username', sanitizedData.username)
.where('id', '!=', playerId)
.first();
if (existingUsername) {
throw new ConflictError('Username is already taken');
}
}
if (Object.keys(sanitizedData).length === 0) {
throw new ValidationError('No valid fields to update');
}
// Update player
sanitizedData.updated_at = new Date();
await db('players')
.where('id', playerId)
.update(sanitizedData);
// Return updated profile
const updatedProfile = await this.getPlayerProfile(playerId, correlationId);
logger.info('Player profile updated successfully', {
correlationId,
playerId,
updatedFields: Object.keys(sanitizedData)
});
return updatedProfile;
} catch (error) {
logger.error('Failed to update player profile', {
correlationId,
playerId,
error: error.message,
stack: error.stack
});
if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) {
throw error;
}
throw new Error('Failed to update player profile');
}
}
/**
* Find player by email
* @param {string} email - Player email
* @returns {Promise<Object|null>} Player data or null
*/
async findPlayerByEmail(email) {
try {
return await db('players')
.where('email', email.toLowerCase().trim())
.first();
} catch (error) {
logger.error('Failed to find player by email', { error: error.message });
return null;
}
}
/**
* Find player by username
* @param {string} username - Player username
* @returns {Promise<Object|null>} Player data or null
*/
async findPlayerByUsername(username) {
try {
return await db('players')
.where('username', username.trim())
.first();
} catch (error) {
logger.error('Failed to find player by username', { error: error.message });
return null;
}
}
/**
* Find player by ID
* @param {number} playerId - Player ID
* @returns {Promise<Object|null>} Player data or null
*/
async findPlayerById(playerId) {
try {
return await db('players')
.where('id', playerId)
.first();
} catch (error) {
logger.error('Failed to find player by ID', { error: error.message });
return null;
}
}
/**
* Validate registration data
* @param {Object} data - Registration data to validate
* @returns {Promise<void>}
* @throws {ValidationError} If validation fails
*/
async validateRegistrationData(data) {
const { email, username, password } = data;
// Validate email
const emailValidation = validateEmail(email);
if (!emailValidation.isValid) {
throw new ValidationError(emailValidation.error);
}
// Validate username
const usernameValidation = validateUsername(username);
if (!usernameValidation.isValid) {
throw new ValidationError(usernameValidation.error);
}
// Validate password strength
const passwordValidation = validatePasswordStrength(password);
if (!passwordValidation.isValid) {
throw new ValidationError('Password does not meet requirements', {
requirements: passwordValidation.requirements,
errors: passwordValidation.errors
});
}
}
}
module.exports = PlayerService;

342
src/utils/jwt.js Normal file
View file

@ -0,0 +1,342 @@
/**
* JWT Token Utilities
* Handles JWT token generation, verification, and management for player and admin authentication
*/
const jwt = require('jsonwebtoken');
const logger = require('./logger');
// JWT Configuration
const JWT_CONFIG = {
player: {
secret: process.env.JWT_PLAYER_SECRET || 'player-secret-change-in-production',
expiresIn: process.env.JWT_PLAYER_EXPIRES_IN || '24h',
issuer: process.env.JWT_ISSUER || 'shattered-void-mmo',
audience: 'player'
},
admin: {
secret: process.env.JWT_ADMIN_SECRET || 'admin-secret-change-in-production',
expiresIn: process.env.JWT_ADMIN_EXPIRES_IN || '8h',
issuer: process.env.JWT_ISSUER || 'shattered-void-mmo',
audience: 'admin'
},
refresh: {
secret: process.env.JWT_REFRESH_SECRET || 'refresh-secret-change-in-production',
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
issuer: process.env.JWT_ISSUER || 'shattered-void-mmo'
}
};
// Validate JWT secrets in production
if (process.env.NODE_ENV === 'production') {
const defaultSecrets = [
'player-secret-change-in-production',
'admin-secret-change-in-production',
'refresh-secret-change-in-production'
];
if (defaultSecrets.includes(JWT_CONFIG.player.secret) ||
defaultSecrets.includes(JWT_CONFIG.admin.secret) ||
defaultSecrets.includes(JWT_CONFIG.refresh.secret)) {
throw new Error('Default JWT secrets detected in production environment. Please set proper JWT secrets.');
}
}
/**
* Generate a JWT token for player authentication
* @param {Object} payload - Token payload
* @param {number} payload.playerId - Player ID
* @param {string} payload.email - Player email
* @param {string} payload.username - Player username
* @param {Object} options - Additional token options
* @returns {string} JWT token
*/
function generatePlayerToken(payload, options = {}) {
try {
const tokenPayload = {
playerId: payload.playerId,
email: payload.email,
username: payload.username,
type: 'player',
iat: Math.floor(Date.now() / 1000),
...options.extraPayload
};
const tokenOptions = {
expiresIn: options.expiresIn || JWT_CONFIG.player.expiresIn,
issuer: JWT_CONFIG.player.issuer,
audience: JWT_CONFIG.player.audience,
subject: payload.playerId.toString()
};
const token = jwt.sign(tokenPayload, JWT_CONFIG.player.secret, tokenOptions);
logger.info('Player JWT token generated', {
playerId: payload.playerId,
username: payload.username,
expiresIn: tokenOptions.expiresIn
});
return token;
} catch (error) {
logger.error('Failed to generate player JWT token:', {
playerId: payload.playerId,
error: error.message
});
throw new Error('Token generation failed');
}
}
/**
* Generate a JWT token for admin authentication
* @param {Object} payload - Token payload
* @param {number} payload.adminId - Admin ID
* @param {string} payload.email - Admin email
* @param {string} payload.username - Admin username
* @param {Array} payload.permissions - Admin permissions
* @param {Object} options - Additional token options
* @returns {string} JWT token
*/
function generateAdminToken(payload, options = {}) {
try {
const tokenPayload = {
adminId: payload.adminId,
email: payload.email,
username: payload.username,
permissions: payload.permissions || [],
type: 'admin',
iat: Math.floor(Date.now() / 1000),
...options.extraPayload
};
const tokenOptions = {
expiresIn: options.expiresIn || JWT_CONFIG.admin.expiresIn,
issuer: JWT_CONFIG.admin.issuer,
audience: JWT_CONFIG.admin.audience,
subject: payload.adminId.toString()
};
const token = jwt.sign(tokenPayload, JWT_CONFIG.admin.secret, tokenOptions);
logger.info('Admin JWT token generated', {
adminId: payload.adminId,
username: payload.username,
permissions: payload.permissions,
expiresIn: tokenOptions.expiresIn
});
return token;
} catch (error) {
logger.error('Failed to generate admin JWT token:', {
adminId: payload.adminId,
error: error.message
});
throw new Error('Token generation failed');
}
}
/**
* Generate a refresh token
* @param {Object} payload - Token payload
* @param {number} payload.userId - User ID
* @param {string} payload.type - Token type ('player' or 'admin')
* @param {Object} options - Additional token options
* @returns {string} Refresh token
*/
function generateRefreshToken(payload, options = {}) {
try {
const tokenPayload = {
userId: payload.userId,
type: payload.type,
tokenId: require('uuid').v4(),
iat: Math.floor(Date.now() / 1000)
};
const tokenOptions = {
expiresIn: options.expiresIn || JWT_CONFIG.refresh.expiresIn,
issuer: JWT_CONFIG.refresh.issuer,
audience: payload.type,
subject: payload.userId.toString()
};
const token = jwt.sign(tokenPayload, JWT_CONFIG.refresh.secret, tokenOptions);
logger.info('Refresh token generated', {
userId: payload.userId,
type: payload.type,
tokenId: tokenPayload.tokenId,
expiresIn: tokenOptions.expiresIn
});
return token;
} catch (error) {
logger.error('Failed to generate refresh token:', {
userId: payload.userId,
type: payload.type,
error: error.message
});
throw new Error('Refresh token generation failed');
}
}
/**
* Verify a player JWT token
* @param {string} token - JWT token to verify
* @returns {Object} Decoded token payload
*/
function verifyPlayerToken(token) {
try {
const decoded = jwt.verify(token, JWT_CONFIG.player.secret, {
issuer: JWT_CONFIG.player.issuer,
audience: JWT_CONFIG.player.audience
});
if (decoded.type !== 'player') {
throw new Error('Invalid token type');
}
return decoded;
} catch (error) {
logger.warn('Player JWT token verification failed:', {
error: error.message,
tokenPrefix: token ? token.substring(0, 20) + '...' : 'null'
});
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
} else {
throw new Error('Token verification failed');
}
}
}
/**
* Verify an admin JWT token
* @param {string} token - JWT token to verify
* @returns {Object} Decoded token payload
*/
function verifyAdminToken(token) {
try {
const decoded = jwt.verify(token, JWT_CONFIG.admin.secret, {
issuer: JWT_CONFIG.admin.issuer,
audience: JWT_CONFIG.admin.audience
});
if (decoded.type !== 'admin') {
throw new Error('Invalid token type');
}
return decoded;
} catch (error) {
logger.warn('Admin JWT token verification failed:', {
error: error.message,
tokenPrefix: token ? token.substring(0, 20) + '...' : 'null'
});
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
} else {
throw new Error('Token verification failed');
}
}
}
/**
* Verify a refresh token
* @param {string} token - Refresh token to verify
* @returns {Object} Decoded token payload
*/
function verifyRefreshToken(token) {
try {
const decoded = jwt.verify(token, JWT_CONFIG.refresh.secret, {
issuer: JWT_CONFIG.refresh.issuer
});
return decoded;
} catch (error) {
logger.warn('Refresh token verification failed:', {
error: error.message,
tokenPrefix: token ? token.substring(0, 20) + '...' : 'null'
});
if (error.name === 'TokenExpiredError') {
throw new Error('Refresh token expired');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid refresh token');
} else {
throw new Error('Refresh token verification failed');
}
}
}
/**
* Decode JWT token without verification (for debugging)
* @param {string} token - JWT token to decode
* @returns {Object} Decoded token payload
*/
function decodeToken(token) {
try {
return jwt.decode(token, { complete: true });
} catch (error) {
logger.error('Failed to decode JWT token:', {
error: error.message,
tokenPrefix: token ? token.substring(0, 20) + '...' : 'null'
});
return null;
}
}
/**
* Check if token is expired
* @param {string} token - JWT token to check
* @returns {boolean} True if token is expired
*/
function isTokenExpired(token) {
try {
const decoded = jwt.decode(token);
if (!decoded || !decoded.exp) return true;
return Date.now() >= decoded.exp * 1000;
} catch (error) {
return true;
}
}
/**
* Extract token from Authorization header
* @param {string} authHeader - Authorization header value
* @returns {string|null} JWT token or null if not found
*/
function extractTokenFromHeader(authHeader) {
if (!authHeader) return null;
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}
module.exports = {
generatePlayerToken,
generateAdminToken,
generateRefreshToken,
verifyPlayerToken,
verifyAdminToken,
verifyRefreshToken,
decodeToken,
isTokenExpired,
extractTokenFromHeader,
JWT_CONFIG
};

74
src/utils/logger.js Normal file
View file

@ -0,0 +1,74 @@
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require('path');
const logDir = path.join(__dirname, '../../logs');
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
);
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'HH:mm:ss' }),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
let log = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0) {
log += ` ${JSON.stringify(meta)}`;
}
return log;
})
);
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: { service: 'shattered-void-mmo' },
transports: [
new DailyRotateFile({
filename: path.join(logDir, 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '14d',
}),
new DailyRotateFile({
filename: path.join(logDir, 'combined-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '30d',
}),
new DailyRotateFile({
filename: path.join(logDir, 'audit-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
level: 'info',
maxSize: '20m',
maxFiles: '90d',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
winston.format((info) => {
return info.audit ? info : false;
})()
),
}),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat,
}));
}
if (process.env.NODE_ENV === 'test') {
logger.transports.forEach((t) => (t.silent = true));
}
logger.audit = (message, meta = {}) => {
logger.info(message, { ...meta, audit: true });
};
module.exports = logger;

329
src/utils/password.js Normal file
View file

@ -0,0 +1,329 @@
/**
* Password Hashing and Validation Utilities
* Handles secure password operations using bcrypt with proper configuration
*/
const bcrypt = require('bcrypt');
const logger = require('./logger');
// Configuration
const BCRYPT_CONFIG = {
saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS) || 12,
maxPasswordLength: parseInt(process.env.MAX_PASSWORD_LENGTH) || 128,
minPasswordLength: parseInt(process.env.MIN_PASSWORD_LENGTH) || 8
};
// Validate salt rounds configuration
if (BCRYPT_CONFIG.saltRounds < 10) {
logger.warn('Low bcrypt salt rounds detected. Consider using 12 or higher for production.');
}
/**
* Hash a password using bcrypt
* @param {string} password - Plain text password to hash
* @returns {Promise<string>} Hashed password
*/
async function hashPassword(password) {
try {
if (!password || typeof password !== 'string') {
throw new Error('Password must be a non-empty string');
}
if (password.length > BCRYPT_CONFIG.maxPasswordLength) {
throw new Error(`Password exceeds maximum length of ${BCRYPT_CONFIG.maxPasswordLength} characters`);
}
if (password.length < BCRYPT_CONFIG.minPasswordLength) {
throw new Error(`Password must be at least ${BCRYPT_CONFIG.minPasswordLength} characters long`);
}
const startTime = Date.now();
const hashedPassword = await bcrypt.hash(password, BCRYPT_CONFIG.saltRounds);
const duration = Date.now() - startTime;
logger.info('Password hashed successfully', {
duration: `${duration}ms`,
saltRounds: BCRYPT_CONFIG.saltRounds
});
return hashedPassword;
} catch (error) {
logger.error('Password hashing failed:', {
error: error.message,
passwordLength: password?.length
});
throw error;
}
}
/**
* Verify a password against its hash
* @param {string} password - Plain text password to verify
* @param {string} hash - Hashed password to compare against
* @returns {Promise<boolean>} True if password matches hash
*/
async function verifyPassword(password, hash) {
try {
if (!password || typeof password !== 'string') {
throw new Error('Password must be a non-empty string');
}
if (!hash || typeof hash !== 'string') {
throw new Error('Hash must be a non-empty string');
}
const startTime = Date.now();
const isValid = await bcrypt.compare(password, hash);
const duration = Date.now() - startTime;
logger.info('Password verification completed', {
duration: `${duration}ms`,
isValid
});
return isValid;
} catch (error) {
logger.error('Password verification failed:', {
error: error.message,
passwordLength: password?.length,
hashLength: hash?.length
});
return false;
}
}
/**
* Check if a hash needs to be rehashed (due to changed salt rounds)
* @param {string} hash - Existing password hash
* @returns {boolean} True if hash needs to be updated
*/
function needsRehash(hash) {
try {
if (!hash || typeof hash !== 'string') {
return true;
}
// Extract salt rounds from hash
const hashParts = hash.split('$');
if (hashParts.length < 4) {
return true;
}
const currentRounds = parseInt(hashParts[2]);
const needsUpdate = currentRounds !== BCRYPT_CONFIG.saltRounds;
if (needsUpdate) {
logger.info('Password hash needs update', {
currentRounds,
targetRounds: BCRYPT_CONFIG.saltRounds
});
}
return needsUpdate;
} catch (error) {
logger.error('Error checking if hash needs rehash:', error);
return true;
}
}
/**
* Rehash password with current salt rounds if needed
* @param {string} password - Plain text password
* @param {string} currentHash - Current password hash
* @returns {Promise<Object>} Result object with hash and wasRehashed flag
*/
async function rehashIfNeeded(password, currentHash) {
try {
const shouldRehash = needsRehash(currentHash);
if (!shouldRehash) {
return {
hash: currentHash,
wasRehashed: false
};
}
const newHash = await hashPassword(password);
logger.info('Password rehashed with updated salt rounds');
return {
hash: newHash,
wasRehashed: true
};
} catch (error) {
logger.error('Password rehashing failed:', error);
throw error;
}
}
/**
* Validate password strength
* @param {string} password - Password to validate
* @returns {Object} Validation result with isValid flag and errors array
*/
function validatePasswordStrength(password) {
const result = {
isValid: true,
errors: [],
score: 0,
requirements: {
minLength: false,
hasUppercase: false,
hasLowercase: false,
hasNumbers: false,
hasSpecialChars: false,
noCommonPatterns: true
}
};
if (!password || typeof password !== 'string') {
result.isValid = false;
result.errors.push('Password must be a string');
return result;
}
// Check minimum length
if (password.length >= BCRYPT_CONFIG.minPasswordLength) {
result.requirements.minLength = true;
result.score += 1;
} else {
result.isValid = false;
result.errors.push(`Password must be at least ${BCRYPT_CONFIG.minPasswordLength} characters long`);
}
// Check maximum length
if (password.length > BCRYPT_CONFIG.maxPasswordLength) {
result.isValid = false;
result.errors.push(`Password exceeds maximum length of ${BCRYPT_CONFIG.maxPasswordLength} characters`);
return result;
}
// Check for uppercase letters
if (/[A-Z]/.test(password)) {
result.requirements.hasUppercase = true;
result.score += 1;
} else {
result.errors.push('Password must contain at least one uppercase letter');
}
// Check for lowercase letters
if (/[a-z]/.test(password)) {
result.requirements.hasLowercase = true;
result.score += 1;
} else {
result.errors.push('Password must contain at least one lowercase letter');
}
// Check for numbers
if (/\d/.test(password)) {
result.requirements.hasNumbers = true;
result.score += 1;
} else {
result.errors.push('Password must contain at least one number');
}
// Check for special characters
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
result.requirements.hasSpecialChars = true;
result.score += 1;
} else {
result.errors.push('Password must contain at least one special character');
}
// Check for common patterns
const commonPatterns = [
/123456/,
/password/i,
/qwerty/i,
/admin/i,
/letmein/i
];
for (const pattern of commonPatterns) {
if (pattern.test(password)) {
result.requirements.noCommonPatterns = false;
result.errors.push('Password contains common patterns that are easily guessable');
break;
}
}
// Check for repeated characters
if (/(.)\1{2,}/.test(password)) {
result.errors.push('Password should not contain repeated characters');
}
// Final validation based on environment
const isProduction = process.env.NODE_ENV === 'production';
const minimumScore = isProduction ? 4 : 3; // Stricter requirements in production
if (result.score < minimumScore) {
result.isValid = false;
if (result.errors.length === 0) {
result.errors.push('Password does not meet strength requirements');
}
}
return result;
}
/**
* Generate a random password
* @param {number} length - Password length (default: 16)
* @param {Object} options - Generation options
* @returns {string} Generated password
*/
function generateRandomPassword(length = 16, options = {}) {
const defaultOptions = {
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSpecialChars: true,
excludeSimilar: true // Exclude similar looking characters
};
const config = { ...defaultOptions, ...options };
let charset = '';
if (config.includeLowercase) {
charset += config.excludeSimilar ? 'abcdefghjkmnpqrstuvwxyz' : 'abcdefghijklmnopqrstuvwxyz';
}
if (config.includeUppercase) {
charset += config.excludeSimilar ? 'ABCDEFGHJKMNPQRSTUVWXYZ' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
if (config.includeNumbers) {
charset += config.excludeSimilar ? '23456789' : '0123456789';
}
if (config.includeSpecialChars) {
charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
}
if (charset === '') {
throw new Error('At least one character type must be included');
}
let password = '';
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
module.exports = {
hashPassword,
verifyPassword,
needsRehash,
rehashIfNeeded,
validatePasswordStrength,
generateRandomPassword,
BCRYPT_CONFIG
};

417
src/utils/redis.js Normal file
View file

@ -0,0 +1,417 @@
/**
* Redis client configuration and utilities
* Handles caching, session storage, and real-time data
*/
const Redis = require('redis');
const logger = require('./logger');
// Create Redis client
const redisClient = Redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
db: parseInt(process.env.REDIS_DB) || 0,
retryDelayOnFailover: 100,
enableReadyCheck: true,
maxRetriesPerRequest: 3,
lazyConnect: true,
});
// Error handling
redisClient.on('error', (error) => {
logger.error('Redis connection error:', error);
});
redisClient.on('connect', () => {
logger.info('Redis client connected');
});
redisClient.on('ready', () => {
logger.info('Redis client ready');
});
redisClient.on('end', () => {
logger.info('Redis connection ended');
});
redisClient.on('reconnecting', () => {
logger.info('Redis client reconnecting...');
});
// Connect to Redis
const connectRedis = async () => {
try {
await redisClient.connect();
logger.info('Connected to Redis successfully');
} catch (error) {
logger.error('Failed to connect to Redis:', error);
throw error;
}
};
// Redis utility methods
const redisUtils = {
/**
* Cache management utilities
*/
cache: {
/**
* Get cached data
* @param {string} key - Cache key
* @returns {Promise<any>} Cached data or null
*/
get: async (key) => {
try {
const data = await redisClient.get(key);
return data ? JSON.parse(data) : null;
} catch (error) {
logger.error('Redis cache get error:', { key, error: error.message });
return null;
}
},
/**
* Set cache data with expiration
* @param {string} key - Cache key
* @param {any} data - Data to cache
* @param {number} ttl - Time to live in seconds (default: 1 hour)
* @returns {Promise<boolean>} Success status
*/
set: async (key, data, ttl = 3600) => {
try {
await redisClient.setEx(key, ttl, JSON.stringify(data));
return true;
} catch (error) {
logger.error('Redis cache set error:', { key, error: error.message });
return false;
}
},
/**
* Delete cached data
* @param {string} key - Cache key
* @returns {Promise<boolean>} Success status
*/
del: async (key) => {
try {
await redisClient.del(key);
return true;
} catch (error) {
logger.error('Redis cache delete error:', { key, error: error.message });
return false;
}
},
/**
* Check if key exists
* @param {string} key - Cache key
* @returns {Promise<boolean>} Exists status
*/
exists: async (key) => {
try {
const result = await redisClient.exists(key);
return result === 1;
} catch (error) {
logger.error('Redis cache exists error:', { key, error: error.message });
return false;
}
},
/**
* Increment a numeric value
* @param {string} key - Cache key
* @param {number} increment - Amount to increment (default: 1)
* @returns {Promise<number>} New value
*/
incr: async (key, increment = 1) => {
try {
return await redisClient.incrBy(key, increment);
} catch (error) {
logger.error('Redis cache increment error:', { key, error: error.message });
return 0;
}
},
},
/**
* Session management utilities
*/
session: {
/**
* Get player session data
* @param {string} sessionId - Session ID
* @returns {Promise<Object|null>} Session data
*/
get: async (sessionId) => {
const key = `session:${sessionId}`;
return redisUtils.cache.get(key);
},
/**
* Set player session data
* @param {string} sessionId - Session ID
* @param {Object} sessionData - Session data
* @param {number} ttl - Session TTL in seconds (default: 24 hours)
* @returns {Promise<boolean>} Success status
*/
set: async (sessionId, sessionData, ttl = 86400) => {
const key = `session:${sessionId}`;
return redisUtils.cache.set(key, sessionData, ttl);
},
/**
* Delete player session
* @param {string} sessionId - Session ID
* @returns {Promise<boolean>} Success status
*/
destroy: async (sessionId) => {
const key = `session:${sessionId}`;
return redisUtils.cache.del(key);
},
/**
* Extend session TTL
* @param {string} sessionId - Session ID
* @param {number} ttl - New TTL in seconds
* @returns {Promise<boolean>} Success status
*/
extend: async (sessionId, ttl = 86400) => {
try {
const key = `session:${sessionId}`;
await redisClient.expire(key, ttl);
return true;
} catch (error) {
logger.error('Redis session extend error:', { sessionId, error: error.message });
return false;
}
},
},
/**
* Real-time event publishing
*/
pubsub: {
/**
* Publish event to channel
* @param {string} channel - Channel name
* @param {Object} data - Event data
* @returns {Promise<boolean>} Success status
*/
publish: async (channel, data) => {
try {
await redisClient.publish(channel, JSON.stringify(data));
return true;
} catch (error) {
logger.error('Redis publish error:', { channel, error: error.message });
return false;
}
},
/**
* Subscribe to channel
* @param {string} channel - Channel name
* @param {Function} callback - Message handler
* @returns {Promise<void>}
*/
subscribe: async (channel, callback) => {
try {
const subscriber = redisClient.duplicate();
await subscriber.connect();
subscriber.on('message', (receivedChannel, message) => {
if (receivedChannel === channel) {
try {
const data = JSON.parse(message);
callback(data);
} catch (error) {
logger.error('Redis message parse error:', { channel, error: error.message });
}
}
});
await subscriber.subscribe(channel);
logger.debug('Subscribed to Redis channel:', channel);
} catch (error) {
logger.error('Redis subscribe error:', { channel, error: error.message });
}
},
},
/**
* Rate limiting utilities
*/
rateLimit: {
/**
* Check and increment rate limit counter
* @param {string} key - Rate limit key (e.g., IP address)
* @param {number} limit - Request limit
* @param {number} window - Time window in seconds
* @returns {Promise<Object>} Rate limit status
*/
check: async (key, limit, window) => {
try {
const rateLimitKey = `ratelimit:${key}`;
const current = await redisClient.incr(rateLimitKey);
if (current === 1) {
await redisClient.expire(rateLimitKey, window);
}
const ttl = await redisClient.ttl(rateLimitKey);
return {
allowed: current <= limit,
count: current,
remaining: Math.max(0, limit - current),
resetTime: Date.now() + (ttl * 1000),
};
} catch (error) {
logger.error('Redis rate limit error:', { key, error: error.message });
return { allowed: true, count: 0, remaining: limit, resetTime: Date.now() + (window * 1000) };
}
},
},
/**
* Game state caching utilities
*/
gameState: {
/**
* Cache player game state
* @param {number} playerId - Player ID
* @param {Object} state - Game state data
* @param {number} ttl - Cache TTL in seconds (default: 5 minutes)
* @returns {Promise<boolean>} Success status
*/
setPlayerState: async (playerId, state, ttl = 300) => {
const key = `gamestate:player:${playerId}`;
return redisUtils.cache.set(key, state, ttl);
},
/**
* Get cached player game state
* @param {number} playerId - Player ID
* @returns {Promise<Object|null>} Game state data
*/
getPlayerState: async (playerId) => {
const key = `gamestate:player:${playerId}`;
return redisUtils.cache.get(key);
},
/**
* Cache colony data
* @param {number} colonyId - Colony ID
* @param {Object} data - Colony data
* @param {number} ttl - Cache TTL in seconds (default: 10 minutes)
* @returns {Promise<boolean>} Success status
*/
setColonyData: async (colonyId, data, ttl = 600) => {
const key = `gamestate:colony:${colonyId}`;
return redisUtils.cache.set(key, data, ttl);
},
/**
* Get cached colony data
* @param {number} colonyId - Colony ID
* @returns {Promise<Object|null>} Colony data
*/
getColonyData: async (colonyId) => {
const key = `gamestate:colony:${colonyId}`;
return redisUtils.cache.get(key);
},
},
/**
* Lock utilities for preventing race conditions
*/
lock: {
/**
* Acquire a distributed lock
* @param {string} key - Lock key
* @param {number} ttl - Lock TTL in seconds (default: 30)
* @returns {Promise<string|null>} Lock token or null if failed
*/
acquire: async (key, ttl = 30) => {
try {
const lockKey = `lock:${key}`;
const token = `${Date.now()}-${Math.random()}`;
const result = await redisClient.set(lockKey, token, {
EX: ttl,
NX: true,
});
return result === 'OK' ? token : null;
} catch (error) {
logger.error('Redis lock acquire error:', { key, error: error.message });
return null;
}
},
/**
* Release a distributed lock
* @param {string} key - Lock key
* @param {string} token - Lock token
* @returns {Promise<boolean>} Success status
*/
release: async (key, token) => {
try {
const lockKey = `lock:${key}`;
const luaScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
const result = await redisClient.eval(luaScript, 1, lockKey, token);
return result === 1;
} catch (error) {
logger.error('Redis lock release error:', { key, error: error.message });
return false;
}
},
},
/**
* Get Redis connection health stats
* @returns {Promise<Object>} Health statistics
*/
getHealthStats: async () => {
try {
const info = await redisClient.info();
const memory = await redisClient.info('memory');
return {
connected: redisClient.isReady,
info: info.split('\r\n').reduce((acc, line) => {
const [key, value] = line.split(':');
if (key && value) acc[key] = value;
return acc;
}, {}),
memory: memory.split('\r\n').reduce((acc, line) => {
const [key, value] = line.split(':');
if (key && value) acc[key] = value;
return acc;
}, {}),
};
} catch (error) {
logger.error('Failed to get Redis health stats:', error);
return { connected: false, error: error.message };
}
},
};
// Initialize Redis connection
if (process.env.NODE_ENV !== 'test') {
connectRedis().catch((error) => {
logger.error('Failed to initialize Redis:', error);
});
}
// Attach utilities to client
Object.assign(redisClient, redisUtils);
module.exports = redisClient;

432
src/utils/validation.js Normal file
View file

@ -0,0 +1,432 @@
/**
* Common Validation Utilities
* Provides reusable validation functions for various data types and formats
*/
const validator = require('validator');
const logger = require('./logger');
/**
* Email validation
* @param {string} email - Email address to validate
* @returns {Object} Validation result with isValid flag and error message
*/
function validateEmail(email) {
const result = { isValid: false, error: null };
if (!email || typeof email !== 'string') {
result.error = 'Email must be a non-empty string';
return result;
}
// Normalize email
const normalizedEmail = email.toLowerCase().trim();
// Check format
if (!validator.isEmail(normalizedEmail)) {
result.error = 'Invalid email format';
return result;
}
// Check length
if (normalizedEmail.length > 320) { // RFC 5321 limit
result.error = 'Email address too long';
return result;
}
// Check for common disposable email domains (optional)
const disposableDomains = [
'10minutemail.com',
'guerrillamail.com',
'mailinator.com',
'tempmail.org'
];
const domain = normalizedEmail.split('@')[1];
if (process.env.BLOCK_DISPOSABLE_EMAILS === 'true' && disposableDomains.includes(domain)) {
result.error = 'Disposable email addresses are not allowed';
return result;
}
result.isValid = true;
return result;
}
/**
* Username validation
* @param {string} username - Username to validate
* @returns {Object} Validation result with isValid flag and error message
*/
function validateUsername(username) {
const result = { isValid: false, error: null };
if (!username || typeof username !== 'string') {
result.error = 'Username must be a non-empty string';
return result;
}
const trimmedUsername = username.trim();
// Check length
const minLength = parseInt(process.env.MIN_USERNAME_LENGTH) || 3;
const maxLength = parseInt(process.env.MAX_USERNAME_LENGTH) || 20;
if (trimmedUsername.length < minLength) {
result.error = `Username must be at least ${minLength} characters long`;
return result;
}
if (trimmedUsername.length > maxLength) {
result.error = `Username must not exceed ${maxLength} characters`;
return result;
}
// Check format (alphanumeric, underscores, hyphens)
if (!/^[a-zA-Z0-9_-]+$/.test(trimmedUsername)) {
result.error = 'Username can only contain letters, numbers, underscores, and hyphens';
return result;
}
// Must start with a letter or number
if (!/^[a-zA-Z0-9]/.test(trimmedUsername)) {
result.error = 'Username must start with a letter or number';
return result;
}
// Must end with a letter or number
if (!/[a-zA-Z0-9]$/.test(trimmedUsername)) {
result.error = 'Username must end with a letter or number';
return result;
}
// Check for reserved usernames
const reservedUsernames = [
'admin', 'administrator', 'root', 'system', 'api', 'www',
'mail', 'email', 'support', 'help', 'info', 'contact',
'null', 'undefined', 'anonymous', 'guest', 'test', 'demo'
];
if (reservedUsernames.includes(trimmedUsername.toLowerCase())) {
result.error = 'Username is reserved and cannot be used';
return result;
}
result.isValid = true;
return result;
}
/**
* Game coordinates validation (format: A3-91-X)
* @param {string} coordinates - Coordinates string to validate
* @returns {Object} Validation result with isValid flag and error message
*/
function validateCoordinates(coordinates) {
const result = { isValid: false, error: null };
if (!coordinates || typeof coordinates !== 'string') {
result.error = 'Coordinates must be a non-empty string';
return result;
}
const trimmedCoords = coordinates.trim().toUpperCase();
// Check format: Letter + Numbers + Hyphen + Numbers + Hyphen + Letter
const coordPattern = /^[A-Z]\d+-\d+-[A-Z]$/;
if (!coordPattern.test(trimmedCoords)) {
result.error = 'Invalid coordinates format. Expected format: A3-91-X';
return result;
}
// Parse components
const parts = trimmedCoords.split('-');
const sector = parts[0]; // A3
const system = parts[1]; // 91
const planet = parts[2]; // X
// Validate sector (Letter followed by 1-2 digits)
if (!/^[A-Z]\d{1,2}$/.test(sector)) {
result.error = 'Invalid sector format in coordinates';
return result;
}
// Validate system (1-3 digits)
const systemNum = parseInt(system);
if (isNaN(systemNum) || systemNum < 1 || systemNum > 999) {
result.error = 'System number must be between 1 and 999';
return result;
}
// Validate planet (single letter)
if (!/^[A-Z]$/.test(planet)) {
result.error = 'Planet identifier must be a single letter';
return result;
}
result.isValid = true;
return result;
}
/**
* Integer validation with range checking
* @param {any} value - Value to validate
* @param {Object} options - Validation options
* @returns {Object} Validation result with isValid flag and error message
*/
function validateInteger(value, options = {}) {
const result = { isValid: false, error: null };
const { min, max, fieldName = 'Value' } = options;
// Check if value exists
if (value === null || value === undefined || value === '') {
result.error = `${fieldName} is required`;
return result;
}
// Convert to number
const numValue = Number(value);
// Check if it's a valid number
if (isNaN(numValue)) {
result.error = `${fieldName} must be a valid number`;
return result;
}
// Check if it's an integer
if (!Number.isInteger(numValue)) {
result.error = `${fieldName} must be an integer`;
return result;
}
// Check minimum value
if (min !== undefined && numValue < min) {
result.error = `${fieldName} must be at least ${min}`;
return result;
}
// Check maximum value
if (max !== undefined && numValue > max) {
result.error = `${fieldName} must not exceed ${max}`;
return result;
}
result.isValid = true;
result.value = numValue;
return result;
}
/**
* String validation with length checking
* @param {string} value - String to validate
* @param {Object} options - Validation options
* @returns {Object} Validation result with isValid flag and error message
*/
function validateString(value, options = {}) {
const result = { isValid: false, error: null };
const {
minLength = 0,
maxLength = 1000,
fieldName = 'Value',
required = true,
trim = true,
allowEmpty = false
} = options;
// Check if value exists
if (value === null || value === undefined) {
if (required) {
result.error = `${fieldName} is required`;
return result;
} else {
result.isValid = true;
result.value = null;
return result;
}
}
// Check if it's a string
if (typeof value !== 'string') {
result.error = `${fieldName} must be a string`;
return result;
}
// Trim if requested
const processedValue = trim ? value.trim() : value;
// Check for empty string
if (!allowEmpty && processedValue === '') {
if (required) {
result.error = `${fieldName} cannot be empty`;
return result;
} else {
result.isValid = true;
result.value = processedValue;
return result;
}
}
// Check minimum length
if (processedValue.length < minLength) {
result.error = `${fieldName} must be at least ${minLength} characters long`;
return result;
}
// Check maximum length
if (processedValue.length > maxLength) {
result.error = `${fieldName} must not exceed ${maxLength} characters`;
return result;
}
result.isValid = true;
result.value = processedValue;
return result;
}
/**
* Array validation
* @param {any} value - Value to validate as array
* @param {Object} options - Validation options
* @returns {Object} Validation result with isValid flag and error message
*/
function validateArray(value, options = {}) {
const result = { isValid: false, error: null };
const {
minLength = 0,
maxLength = 100,
fieldName = 'Array',
required = true,
itemValidator = null
} = options;
// Check if value exists
if (value === null || value === undefined) {
if (required) {
result.error = `${fieldName} is required`;
return result;
} else {
result.isValid = true;
result.value = [];
return result;
}
}
// Check if it's an array
if (!Array.isArray(value)) {
result.error = `${fieldName} must be an array`;
return result;
}
// Check minimum length
if (value.length < minLength) {
result.error = `${fieldName} must contain at least ${minLength} items`;
return result;
}
// Check maximum length
if (value.length > maxLength) {
result.error = `${fieldName} must not contain more than ${maxLength} items`;
return result;
}
// Validate individual items if validator provided
if (itemValidator && typeof itemValidator === 'function') {
for (let i = 0; i < value.length; i++) {
const itemResult = itemValidator(value[i], i);
if (!itemResult.isValid) {
result.error = `${fieldName}[${i}]: ${itemResult.error}`;
return result;
}
}
}
result.isValid = true;
result.value = value;
return result;
}
/**
* UUID validation
* @param {string} uuid - UUID string to validate
* @param {Object} options - Validation options
* @returns {Object} Validation result with isValid flag and error message
*/
function validateUUID(uuid, options = {}) {
const result = { isValid: false, error: null };
const { fieldName = 'UUID', required = true, version = null } = options;
// Check if value exists
if (!uuid) {
if (required) {
result.error = `${fieldName} is required`;
return result;
} else {
result.isValid = true;
result.value = null;
return result;
}
}
// Check if it's a string
if (typeof uuid !== 'string') {
result.error = `${fieldName} must be a string`;
return result;
}
// Validate UUID format
const uuidValidation = version ?
validator.isUUID(uuid, version) :
validator.isUUID(uuid);
if (!uuidValidation) {
result.error = `Invalid ${fieldName} format`;
return result;
}
result.isValid = true;
result.value = uuid;
return result;
}
/**
* Sanitize HTML content
* @param {string} html - HTML content to sanitize
* @returns {string} Sanitized HTML
*/
function sanitizeHTML(html) {
if (!html || typeof html !== 'string') {
return '';
}
// Basic HTML sanitization - remove script tags and dangerous attributes
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/gi, '')
.replace(/on\w+='[^']*'/gi, '')
.replace(/javascript:/gi, '')
.replace(/vbscript:/gi, '')
.replace(/data:/gi, '');
}
/**
* Rate limiting key generator
* @param {Object} req - Express request object
* @param {string} action - Action being rate limited
* @returns {string} Rate limiting key
*/
function generateRateLimitKey(req, action) {
const ip = req.ip || req.connection.remoteAddress;
const userId = req.user?.playerId || req.user?.adminId || 'anonymous';
return `ratelimit:${action}:${userId}:${ip}`;
}
module.exports = {
validateEmail,
validateUsername,
validateCoordinates,
validateInteger,
validateString,
validateArray,
validateUUID,
sanitizeHTML,
generateRateLimitKey
};

431
src/utils/websocket.js Normal file
View file

@ -0,0 +1,431 @@
/**
* WebSocket connection and event handling
*/
const jwt = require('jsonwebtoken');
const logger = require('./logger');
const db = require('../database/connection');
/**
* Initialize WebSocket handlers
* @param {Object} io - Socket.io instance
*/
function initializeWebSocketHandlers(io) {
// Middleware for authentication
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication token required'));
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from database
const tableName = decoded.type === 'admin' ? 'admin_users' : 'players';
const user = await db(tableName)
.where('id', decoded.userId)
.first();
if (!user) {
return next(new Error('User not found'));
}
// Check if user is active
if (decoded.type === 'player' && user.account_status !== 'active') {
return next(new Error('Account is not active'));
}
if (decoded.type === 'admin' && !user.is_active) {
return next(new Error('Admin account is not active'));
}
// Attach user info to socket
socket.user = user;
socket.userType = decoded.type;
next();
} catch (error) {
logger.warn('WebSocket authentication failed', {
error: error.message,
socketId: socket.id,
});
next(new Error('Authentication failed'));
}
});
// Handle connections
io.on('connection', async (socket) => {
try {
// Store connection in database
await storeConnection(socket);
logger.info('WebSocket connection established', {
socketId: socket.id,
userId: socket.user.id,
userType: socket.userType,
username: socket.user.username,
});
// Set up event handlers
setupEventHandlers(socket, io);
// Handle disconnection
socket.on('disconnect', async (reason) => {
await handleDisconnection(socket, reason);
});
} catch (error) {
logger.error('Error handling WebSocket connection:', error);
socket.disconnect(true);
}
});
logger.info('WebSocket handlers initialized');
}
/**
* Store WebSocket connection in database
* @param {Object} socket - Socket instance
*/
async function storeConnection(socket) {
try {
await db('websocket_connections').insert({
connection_id: socket.id,
player_id: socket.userType === 'player' ? socket.user.id : null,
admin_id: socket.userType === 'admin' ? socket.user.id : null,
user_type: socket.userType,
connected_at: new Date(),
last_ping: new Date(),
ip_address: socket.handshake.address,
user_agent: socket.handshake.headers['user-agent'],
});
} catch (error) {
logger.error('Failed to store WebSocket connection:', error);
}
}
/**
* Set up event handlers for socket
* @param {Object} socket - Socket instance
* @param {Object} io - Socket.io instance
*/
function setupEventHandlers(socket, io) {
// Ping/pong for connection health
socket.on('ping', (callback) => {
updateLastPing(socket.id);
if (callback) callback('pong');
});
// Subscribe to specific event types
socket.on('subscribe', async (data, callback) => {
try {
await handleSubscription(socket, data);
if (callback) callback({ success: true });
} catch (error) {
logger.error('Subscription error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
// Unsubscribe from event types
socket.on('unsubscribe', async (data, callback) => {
try {
await handleUnsubscription(socket, data);
if (callback) callback({ success: true });
} catch (error) {
logger.error('Unsubscription error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
// Player-specific events
if (socket.userType === 'player') {
setupPlayerEventHandlers(socket, io);
}
// Admin-specific events
if (socket.userType === 'admin') {
setupAdminEventHandlers(socket, io);
}
}
/**
* Set up player-specific event handlers
* @param {Object} socket - Socket instance
* @param {Object} io - Socket.io instance
*/
function setupPlayerEventHandlers(socket, io) {
// Join player-specific room
socket.join(`player:${socket.user.id}`);
// Game action events
socket.on('game:action', async (data, callback) => {
try {
// TODO: Handle game actions
logger.debug('Game action received', {
playerId: socket.user.id,
action: data.action,
data: data,
});
if (callback) callback({ success: true });
} catch (error) {
logger.error('Game action error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
// Colony events
socket.on('colony:update', async (data, callback) => {
try {
// TODO: Handle colony updates
if (callback) callback({ success: true });
} catch (error) {
logger.error('Colony update error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
// Fleet events
socket.on('fleet:move', async (data, callback) => {
try {
// TODO: Handle fleet movements
if (callback) callback({ success: true });
} catch (error) {
logger.error('Fleet move error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
}
/**
* Set up admin-specific event handlers
* @param {Object} socket - Socket instance
* @param {Object} io - Socket.io instance
*/
function setupAdminEventHandlers(socket, io) {
// Join admin room
socket.join('admin');
// System events
socket.on('admin:system', async (data, callback) => {
try {
// TODO: Handle admin system commands
logger.info('Admin system command', {
adminId: socket.user.id,
command: data.command,
});
if (callback) callback({ success: true });
} catch (error) {
logger.error('Admin system error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
// Player management events
socket.on('admin:player', async (data, callback) => {
try {
// TODO: Handle admin player management
if (callback) callback({ success: true });
} catch (error) {
logger.error('Admin player error:', error);
if (callback) callback({ success: false, error: error.message });
}
});
}
/**
* Handle subscription requests
* @param {Object} socket - Socket instance
* @param {Object} data - Subscription data
*/
async function handleSubscription(socket, data) {
const { subscriptionType, scopeType, scopeId, filters } = data;
// Validate subscription request
if (!subscriptionType) {
throw new Error('Subscription type required');
}
// Check permissions
if (!canSubscribe(socket, subscriptionType, scopeType, scopeId)) {
throw new Error('Insufficient permissions for subscription');
}
// Store subscription in database
await db('websocket_subscriptions')
.insert({
connection_id: socket.id,
subscription_type: subscriptionType,
scope_type: scopeType || null,
scope_id: scopeId || null,
filters: filters ? JSON.stringify(filters) : '{}',
})
.onConflict(['connection_id', 'subscription_type', 'scope_type', 'scope_id'])
.merge();
// Join appropriate socket room
const roomName = getRoomName(subscriptionType, scopeType, scopeId);
socket.join(roomName);
logger.debug('WebSocket subscription added', {
socketId: socket.id,
userId: socket.user.id,
subscriptionType,
scopeType,
scopeId,
room: roomName,
});
}
/**
* Handle unsubscription requests
* @param {Object} socket - Socket instance
* @param {Object} data - Unsubscription data
*/
async function handleUnsubscription(socket, data) {
const { subscriptionType, scopeType, scopeId } = data;
// Remove subscription from database
await db('websocket_subscriptions')
.where('connection_id', socket.id)
.where('subscription_type', subscriptionType)
.where('scope_type', scopeType || null)
.where('scope_id', scopeId || null)
.del();
// Leave socket room
const roomName = getRoomName(subscriptionType, scopeType, scopeId);
socket.leave(roomName);
logger.debug('WebSocket subscription removed', {
socketId: socket.id,
subscriptionType,
scopeType,
scopeId,
room: roomName,
});
}
/**
* Check if user can subscribe to event type
* @param {Object} socket - Socket instance
* @param {string} subscriptionType - Subscription type
* @param {string} scopeType - Scope type
* @param {number} scopeId - Scope ID
* @returns {boolean} Can subscribe
*/
function canSubscribe(socket, subscriptionType, scopeType, scopeId) {
// Global events - anyone can subscribe
if (subscriptionType === 'global_events') {
return true;
}
// Player-specific events
if (socket.userType === 'player') {
if (subscriptionType === 'colony_updates' && scopeType === 'player' && scopeId === socket.user.id) {
return true;
}
if (subscriptionType === 'fleet_movements' && scopeType === 'player' && scopeId === socket.user.id) {
return true;
}
// TODO: Add more player subscription checks
}
// Admin events
if (socket.userType === 'admin') {
// Admins can subscribe to most events
return true;
}
return false;
}
/**
* Generate room name for subscription
* @param {string} subscriptionType - Subscription type
* @param {string} scopeType - Scope type
* @param {number} scopeId - Scope ID
* @returns {string} Room name
*/
function getRoomName(subscriptionType, scopeType, scopeId) {
if (!scopeType) {
return subscriptionType;
}
return `${subscriptionType}:${scopeType}:${scopeId}`;
}
/**
* Update last ping timestamp
* @param {string} connectionId - Connection ID
*/
async function updateLastPing(connectionId) {
try {
await db('websocket_connections')
.where('connection_id', connectionId)
.update({ last_ping: new Date() });
} catch (error) {
logger.error('Failed to update last ping:', error);
}
}
/**
* Handle WebSocket disconnection
* @param {Object} socket - Socket instance
* @param {string} reason - Disconnection reason
*/
async function handleDisconnection(socket, reason) {
try {
// Remove connection from database
await db('websocket_connections')
.where('connection_id', socket.id)
.update({ is_active: false });
// Remove all subscriptions
await db('websocket_subscriptions')
.where('connection_id', socket.id)
.del();
logger.info('WebSocket disconnected', {
socketId: socket.id,
userId: socket.user?.id,
userType: socket.userType,
reason,
});
} catch (error) {
logger.error('Error handling WebSocket disconnection:', error);
}
}
/**
* Broadcast event to subscribed connections
* @param {Object} io - Socket.io instance
* @param {string} eventType - Event type
* @param {Object} data - Event data
* @param {string} scopeType - Scope type (optional)
* @param {number} scopeId - Scope ID (optional)
*/
function broadcastEvent(io, eventType, data, scopeType = null, scopeId = null) {
const roomName = getRoomName(eventType, scopeType, scopeId);
io.to(roomName).emit(eventType, {
type: eventType,
data,
timestamp: new Date().toISOString(),
});
logger.debug('Event broadcasted', {
eventType,
room: roomName,
dataKeys: Object.keys(data),
});
}
module.exports = {
initializeWebSocketHandlers,
broadcastEvent,
};