feat: implement complete Phase 2 frontend foundation with React 18

Major milestone: Frontend implementation complete for Shattered Void MMO

FRONTEND IMPLEMENTATION:
- React 18 + TypeScript + Vite development environment
- Tailwind CSS with custom dark theme for sci-fi aesthetic
- Zustand state management with authentication persistence
- Socket.io WebSocket client with auto-reconnection
- Protected routing with authentication guards
- Responsive design with mobile-first approach

AUTHENTICATION SYSTEM:
- Login/register forms with comprehensive validation
- JWT token management with localStorage persistence
- Password strength validation and user feedback
- Protected routes and authentication guards

CORE GAME INTERFACE:
- Colony management dashboard with real-time updates
- Resource display with live production tracking
- WebSocket integration for real-time game events
- Navigation with connection status indicator
- Toast notifications for user feedback

BACKEND ENHANCEMENTS:
- Complete Research System with technology tree (23 technologies)
- Fleet Management System with ship designs and movement
- Enhanced Authentication with email verification and password reset
- Complete game tick integration for all systems
- Advanced WebSocket events for real-time updates

ARCHITECTURE FEATURES:
- Type-safe TypeScript throughout
- Component-based architecture with reusable UI elements
- API client with request/response interceptors
- Error handling and loading states
- Performance optimized builds with code splitting

Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met)
Ready for: Colony management, fleet operations, research interface
Next: Enhanced gameplay features and admin interface

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MegaProxy 2025-08-02 18:36:06 +00:00
parent 8d9ef427be
commit d41d1e8125
130 changed files with 33588 additions and 14817 deletions

View file

@ -29,22 +29,22 @@ 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',
combat: '/api/admin/combat'
},
note: 'Administrative access required for all endpoints'
});
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',
combat: '/api/admin/combat',
},
note: 'Administrative access required for all endpoints',
});
});
/**
@ -55,50 +55,50 @@ const authRoutes = express.Router();
// Public admin authentication endpoints
authRoutes.post('/login',
rateLimiters.auth,
validators.validateAdminLogin,
auditAdminAction('admin_login'),
adminAuthController.login
rateLimiters.auth,
validators.validateAdminLogin,
auditAdminAction('admin_login'),
adminAuthController.login,
);
// Protected admin authentication endpoints
authRoutes.post('/logout',
authenticateAdmin,
auditAdminAction('admin_logout'),
adminAuthController.logout
authenticateAdmin,
auditAdminAction('admin_logout'),
adminAuthController.logout,
);
authRoutes.get('/me',
authenticateAdmin,
adminAuthController.getProfile
authenticateAdmin,
adminAuthController.getProfile,
);
authRoutes.get('/verify',
authenticateAdmin,
adminAuthController.verifyToken
authenticateAdmin,
adminAuthController.verifyToken,
);
authRoutes.post('/refresh',
rateLimiters.auth,
adminAuthController.refresh
rateLimiters.auth,
adminAuthController.refresh,
);
authRoutes.get('/stats',
authenticateAdmin,
requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]),
auditAdminAction('view_system_stats'),
adminAuthController.getSystemStats
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
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
@ -115,125 +115,125 @@ 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;
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);
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
});
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
});
}
} 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);
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
});
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
});
}
} 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;
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
);
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
});
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
});
}
} 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
@ -250,88 +250,88 @@ 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);
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)
}
}
};
// 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
});
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
});
}
} 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
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()
}
};
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
});
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
});
}
} catch (error) {
res.status(500).json({
success: false,
error: 'Health check failed',
message: error.message,
correlationId: req.correlationId,
});
}
},
);
// Mount system routes
@ -348,26 +348,26 @@ router.use('/combat', require('./admin/combat'));
* /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
});
}
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,
});
},
);
/**
@ -375,34 +375,34 @@ router.get('/events',
* /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
});
}
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()
});
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;
module.exports = router;

View file

@ -8,21 +8,21 @@ const router = express.Router();
// Import controllers
const {
getCombatStatistics,
getCombatQueue,
forceResolveCombat,
cancelBattle,
getCombatConfigurations,
saveCombatConfiguration,
deleteCombatConfiguration
getCombatStatistics,
getCombatQueue,
forceResolveCombat,
cancelBattle,
getCombatConfigurations,
saveCombatConfiguration,
deleteCombatConfiguration,
} = require('../../controllers/admin/combat.controller');
// Import middleware
const { authenticateAdmin } = require('../../middleware/admin.middleware');
const {
validateCombatQueueQuery,
validateParams,
logCombatAction
validateCombatQueueQuery,
validateParams,
logCombatAction,
} = require('../../middleware/combat.middleware');
const { validateCombatConfiguration } = require('../../validators/combat.validators');
@ -35,8 +35,8 @@ router.use(authenticateAdmin);
* @access Admin
*/
router.get('/statistics',
logCombatAction('admin_get_combat_statistics'),
getCombatStatistics
logCombatAction('admin_get_combat_statistics'),
getCombatStatistics,
);
/**
@ -45,9 +45,9 @@ router.get('/statistics',
* @access Admin
*/
router.get('/queue',
logCombatAction('admin_get_combat_queue'),
validateCombatQueueQuery,
getCombatQueue
logCombatAction('admin_get_combat_queue'),
validateCombatQueueQuery,
getCombatQueue,
);
/**
@ -56,9 +56,9 @@ router.get('/queue',
* @access Admin
*/
router.post('/resolve/:battleId',
logCombatAction('admin_force_resolve_combat'),
validateParams('battleId'),
forceResolveCombat
logCombatAction('admin_force_resolve_combat'),
validateParams('battleId'),
forceResolveCombat,
);
/**
@ -67,20 +67,20 @@ router.post('/resolve/:battleId',
* @access Admin
*/
router.post('/cancel/:battleId',
logCombatAction('admin_cancel_battle'),
validateParams('battleId'),
(req, res, next) => {
// Validate cancel reason in request body
const { reason } = req.body;
if (!reason || typeof reason !== 'string' || reason.trim().length < 5) {
return res.status(400).json({
error: 'Cancel reason is required and must be at least 5 characters',
code: 'INVALID_CANCEL_REASON'
});
}
next();
},
cancelBattle
logCombatAction('admin_cancel_battle'),
validateParams('battleId'),
(req, res, next) => {
// Validate cancel reason in request body
const { reason } = req.body;
if (!reason || typeof reason !== 'string' || reason.trim().length < 5) {
return res.status(400).json({
error: 'Cancel reason is required and must be at least 5 characters',
code: 'INVALID_CANCEL_REASON',
});
}
next();
},
cancelBattle,
);
/**
@ -89,8 +89,8 @@ router.post('/cancel/:battleId',
* @access Admin
*/
router.get('/configurations',
logCombatAction('admin_get_combat_configurations'),
getCombatConfigurations
logCombatAction('admin_get_combat_configurations'),
getCombatConfigurations,
);
/**
@ -99,25 +99,25 @@ router.get('/configurations',
* @access Admin
*/
router.post('/configurations',
logCombatAction('admin_create_combat_configuration'),
(req, res, next) => {
const { error, value } = validateCombatConfiguration(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details
});
}
req.body = value;
next();
},
saveCombatConfiguration
logCombatAction('admin_create_combat_configuration'),
(req, res, next) => {
const { error, value } = validateCombatConfiguration(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
}));
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details,
});
}
req.body = value;
next();
},
saveCombatConfiguration,
);
/**
@ -126,26 +126,26 @@ router.post('/configurations',
* @access Admin
*/
router.put('/configurations/:configId',
logCombatAction('admin_update_combat_configuration'),
validateParams('configId'),
(req, res, next) => {
const { error, value } = validateCombatConfiguration(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details
});
}
req.body = value;
next();
},
saveCombatConfiguration
logCombatAction('admin_update_combat_configuration'),
validateParams('configId'),
(req, res, next) => {
const { error, value } = validateCombatConfiguration(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
}));
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details,
});
}
req.body = value;
next();
},
saveCombatConfiguration,
);
/**
@ -154,9 +154,9 @@ router.put('/configurations/:configId',
* @access Admin
*/
router.delete('/configurations/:configId',
logCombatAction('admin_delete_combat_configuration'),
validateParams('configId'),
deleteCombatConfiguration
logCombatAction('admin_delete_combat_configuration'),
validateParams('configId'),
deleteCombatConfiguration,
);
/**
@ -165,98 +165,98 @@ router.delete('/configurations/:configId',
* @access Admin
*/
router.get('/battles',
logCombatAction('admin_get_battles'),
async (req, res, next) => {
try {
const {
status,
battle_type,
location,
limit = 50,
offset = 0,
start_date,
end_date
} = req.query;
logCombatAction('admin_get_battles'),
async (req, res, next) => {
try {
const {
status,
battle_type,
location,
limit = 50,
offset = 0,
start_date,
end_date,
} = req.query;
const db = require('../../database/connection');
const logger = require('../../utils/logger');
const db = require('../../database/connection');
const logger = require('../../utils/logger');
let query = db('battles')
.select([
'battles.*',
'combat_configurations.config_name',
'combat_configurations.combat_type'
])
.leftJoin('combat_configurations', 'battles.combat_configuration_id', 'combat_configurations.id')
.orderBy('battles.started_at', 'desc')
.limit(parseInt(limit))
.offset(parseInt(offset));
let query = db('battles')
.select([
'battles.*',
'combat_configurations.config_name',
'combat_configurations.combat_type',
])
.leftJoin('combat_configurations', 'battles.combat_configuration_id', 'combat_configurations.id')
.orderBy('battles.started_at', 'desc')
.limit(parseInt(limit))
.offset(parseInt(offset));
if (status) {
query = query.where('battles.status', status);
}
if (status) {
query = query.where('battles.status', status);
}
if (battle_type) {
query = query.where('battles.battle_type', battle_type);
}
if (battle_type) {
query = query.where('battles.battle_type', battle_type);
}
if (location) {
query = query.where('battles.location', location);
}
if (location) {
query = query.where('battles.location', location);
}
if (start_date) {
query = query.where('battles.started_at', '>=', new Date(start_date));
}
if (start_date) {
query = query.where('battles.started_at', '>=', new Date(start_date));
}
if (end_date) {
query = query.where('battles.started_at', '<=', new Date(end_date));
}
if (end_date) {
query = query.where('battles.started_at', '<=', new Date(end_date));
}
const battles = await query;
const battles = await query;
// Get total count for pagination
let countQuery = db('battles').count('* as total');
if (status) countQuery = countQuery.where('status', status);
if (battle_type) countQuery = countQuery.where('battle_type', battle_type);
if (location) countQuery = countQuery.where('location', location);
if (start_date) countQuery = countQuery.where('started_at', '>=', new Date(start_date));
if (end_date) countQuery = countQuery.where('started_at', '<=', new Date(end_date));
// Get total count for pagination
let countQuery = db('battles').count('* as total');
const [{ total }] = await countQuery;
if (status) countQuery = countQuery.where('status', status);
if (battle_type) countQuery = countQuery.where('battle_type', battle_type);
if (location) countQuery = countQuery.where('location', location);
if (start_date) countQuery = countQuery.where('started_at', '>=', new Date(start_date));
if (end_date) countQuery = countQuery.where('started_at', '<=', new Date(end_date));
// Parse participants JSON for each battle
const battlesWithParsedParticipants = battles.map(battle => ({
...battle,
participants: JSON.parse(battle.participants),
battle_data: battle.battle_data ? JSON.parse(battle.battle_data) : null,
result: battle.result ? JSON.parse(battle.result) : null
}));
const [{ total }] = await countQuery;
logger.info('Admin battles retrieved', {
correlationId: req.correlationId,
adminUser: req.user.id,
count: battles.length,
total: parseInt(total)
});
// Parse participants JSON for each battle
const battlesWithParsedParticipants = battles.map(battle => ({
...battle,
participants: JSON.parse(battle.participants),
battle_data: battle.battle_data ? JSON.parse(battle.battle_data) : null,
result: battle.result ? JSON.parse(battle.result) : null,
}));
res.json({
success: true,
data: {
battles: battlesWithParsedParticipants,
pagination: {
total: parseInt(total),
limit: parseInt(limit),
offset: parseInt(offset),
hasMore: (parseInt(offset) + parseInt(limit)) < parseInt(total)
}
}
});
logger.info('Admin battles retrieved', {
correlationId: req.correlationId,
adminUser: req.user.id,
count: battles.length,
total: parseInt(total),
});
} catch (error) {
next(error);
}
res.json({
success: true,
data: {
battles: battlesWithParsedParticipants,
pagination: {
total: parseInt(total),
limit: parseInt(limit),
offset: parseInt(offset),
hasMore: (parseInt(offset) + parseInt(limit)) < parseInt(total),
},
},
});
} catch (error) {
next(error);
}
},
);
/**
@ -265,81 +265,81 @@ router.get('/battles',
* @access Admin
*/
router.get('/encounters/:encounterId',
logCombatAction('admin_get_combat_encounter'),
validateParams('encounterId'),
async (req, res, next) => {
try {
const encounterId = parseInt(req.params.encounterId);
const db = require('../../database/connection');
const logger = require('../../utils/logger');
logCombatAction('admin_get_combat_encounter'),
validateParams('encounterId'),
async (req, res, next) => {
try {
const encounterId = parseInt(req.params.encounterId);
const db = require('../../database/connection');
const logger = require('../../utils/logger');
// Get encounter with all related data
const encounter = await db('combat_encounters')
.select([
'combat_encounters.*',
'battles.battle_type',
'battles.participants',
'battles.started_at as battle_started',
'battles.completed_at as battle_completed',
'attacker_fleet.name as attacker_fleet_name',
'attacker_player.username as attacker_username',
'defender_fleet.name as defender_fleet_name',
'defender_player.username as defender_username',
'defender_colony.name as defender_colony_name',
'colony_player.username as colony_owner_username'
])
.join('battles', 'combat_encounters.battle_id', 'battles.id')
.leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id')
.leftJoin('players as attacker_player', 'attacker_fleet.player_id', 'attacker_player.id')
.leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id')
.leftJoin('players as defender_player', 'defender_fleet.player_id', 'defender_player.id')
.leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id')
.leftJoin('players as colony_player', 'defender_colony.player_id', 'colony_player.id')
.where('combat_encounters.id', encounterId)
.first();
// Get encounter with all related data
const encounter = await db('combat_encounters')
.select([
'combat_encounters.*',
'battles.battle_type',
'battles.participants',
'battles.started_at as battle_started',
'battles.completed_at as battle_completed',
'attacker_fleet.name as attacker_fleet_name',
'attacker_player.username as attacker_username',
'defender_fleet.name as defender_fleet_name',
'defender_player.username as defender_username',
'defender_colony.name as defender_colony_name',
'colony_player.username as colony_owner_username',
])
.join('battles', 'combat_encounters.battle_id', 'battles.id')
.leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id')
.leftJoin('players as attacker_player', 'attacker_fleet.player_id', 'attacker_player.id')
.leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id')
.leftJoin('players as defender_player', 'defender_fleet.player_id', 'defender_player.id')
.leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id')
.leftJoin('players as colony_player', 'defender_colony.player_id', 'colony_player.id')
.where('combat_encounters.id', encounterId)
.first();
if (!encounter) {
return res.status(404).json({
error: 'Combat encounter not found',
code: 'ENCOUNTER_NOT_FOUND'
});
}
if (!encounter) {
return res.status(404).json({
error: 'Combat encounter not found',
code: 'ENCOUNTER_NOT_FOUND',
});
}
// Get combat logs
const combatLogs = await db('combat_logs')
.where('encounter_id', encounterId)
.orderBy('round_number')
.orderBy('timestamp');
// Get combat logs
const combatLogs = await db('combat_logs')
.where('encounter_id', encounterId)
.orderBy('round_number')
.orderBy('timestamp');
const detailedEncounter = {
...encounter,
participants: JSON.parse(encounter.participants),
initial_forces: JSON.parse(encounter.initial_forces),
final_forces: JSON.parse(encounter.final_forces),
casualties: JSON.parse(encounter.casualties),
combat_log: JSON.parse(encounter.combat_log),
loot_awarded: JSON.parse(encounter.loot_awarded),
detailed_logs: combatLogs.map(log => ({
...log,
event_data: JSON.parse(log.event_data)
}))
};
const detailedEncounter = {
...encounter,
participants: JSON.parse(encounter.participants),
initial_forces: JSON.parse(encounter.initial_forces),
final_forces: JSON.parse(encounter.final_forces),
casualties: JSON.parse(encounter.casualties),
combat_log: JSON.parse(encounter.combat_log),
loot_awarded: JSON.parse(encounter.loot_awarded),
detailed_logs: combatLogs.map(log => ({
...log,
event_data: JSON.parse(log.event_data),
})),
};
logger.info('Admin combat encounter retrieved', {
correlationId: req.correlationId,
adminUser: req.user.id,
encounterId
});
logger.info('Admin combat encounter retrieved', {
correlationId: req.correlationId,
adminUser: req.user.id,
encounterId,
});
res.json({
success: true,
data: detailedEncounter
});
res.json({
success: true,
data: detailedEncounter,
});
} catch (error) {
next(error);
}
} catch (error) {
next(error);
}
},
);
module.exports = router;
module.exports = router;

View file

@ -39,4 +39,4 @@ router.get('/status', authenticateToken('admin'), asyncHandler(async (req, res)
});
}));
module.exports = router;
module.exports = router;

View file

@ -6,10 +6,10 @@
const express = require('express');
const router = express.Router();
const logger = require('../../utils/logger');
const {
gameTickService,
getGameTickStatus,
triggerManualTick
const {
gameTickService,
getGameTickStatus,
triggerManualTick,
} = require('../../services/game-tick.service');
const db = require('../../database/connection');
const { v4: uuidv4 } = require('uuid');
@ -20,22 +20,22 @@ const { v4: uuidv4 } = require('uuid');
*/
router.get('/tick/status', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.info('Admin requesting game tick status', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
adminUsername: req.user?.username,
});
const status = getGameTickStatus();
// Get recent tick logs
const recentLogs = await db('game_tick_log')
.select('*')
.orderBy('tick_number', 'desc')
.limit(10);
// Get performance statistics
const performanceStats = await db('game_tick_log')
.select(
@ -43,20 +43,20 @@ router.get('/tick/status', async (req, res) => {
db.raw('COUNT(*) as total_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'completed\') as successful_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failed_ticks'),
db.raw('MAX(tick_number) as latest_tick')
db.raw('MAX(tick_number) as latest_tick'),
)
.where('started_at', '>=', db.raw("NOW() - INTERVAL '24 hours'"))
.where('started_at', '>=', db.raw('NOW() - INTERVAL \'24 hours\''))
.first();
// Get user group statistics
const userGroupStats = await db('game_tick_log')
.select(
'user_group',
db.raw('COUNT(*) as tick_count'),
db.raw('AVG(processed_players) as avg_players'),
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failures')
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failures'),
)
.where('started_at', '>=', db.raw("NOW() - INTERVAL '24 hours'"))
.where('started_at', '>=', db.raw('NOW() - INTERVAL \'24 hours\''))
.groupBy('user_group')
.orderBy('user_group');
@ -75,11 +75,11 @@ router.get('/tick/status', async (req, res) => {
duration: log.performance_metrics?.duration_ms,
startedAt: log.started_at,
completedAt: log.completed_at,
errorMessage: log.error_message
}))
errorMessage: log.error_message,
})),
},
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
@ -87,13 +87,13 @@ router.get('/tick/status', async (req, res) => {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
stack: error.stack,
});
res.status(500).json({
success: false,
error: 'Failed to retrieve game tick status',
correlationId
correlationId,
});
}
});
@ -104,16 +104,16 @@ router.get('/tick/status', async (req, res) => {
*/
router.post('/tick/trigger', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.info('Admin triggering manual game tick', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
adminUsername: req.user?.username,
});
const result = await triggerManualTick(correlationId);
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick',
@ -123,10 +123,10 @@ router.post('/tick/trigger', async (req, res) => {
actor_id: req.user?.id,
changes: {
correlation_id: correlationId,
triggered_by: req.user?.username
triggered_by: req.user?.username,
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
user_agent: req.get('User-Agent'),
});
res.json({
@ -134,7 +134,7 @@ router.post('/tick/trigger', async (req, res) => {
message: 'Manual game tick triggered successfully',
data: result,
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
@ -142,13 +142,13 @@ router.post('/tick/trigger', async (req, res) => {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
stack: error.stack,
});
res.status(500).json({
success: false,
error: error.message || 'Failed to trigger manual game tick',
correlationId
correlationId,
});
}
});
@ -159,34 +159,34 @@ router.post('/tick/trigger', async (req, res) => {
*/
router.put('/tick/config', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
const {
tick_interval_ms,
user_groups_count,
max_retry_attempts,
bonus_tick_threshold,
retry_delay_ms
retry_delay_ms,
} = req.body;
logger.info('Admin updating game tick configuration', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username,
newConfig: req.body
newConfig: req.body,
});
// Validate configuration values
const validationErrors = [];
if (tick_interval_ms && (tick_interval_ms < 10000 || tick_interval_ms > 3600000)) {
validationErrors.push('tick_interval_ms must be between 10000 and 3600000 (10 seconds to 1 hour)');
}
if (user_groups_count && (user_groups_count < 1 || user_groups_count > 50)) {
validationErrors.push('user_groups_count must be between 1 and 50');
}
if (max_retry_attempts && (max_retry_attempts < 1 || max_retry_attempts > 10)) {
validationErrors.push('max_retry_attempts must be between 1 and 10');
}
@ -196,7 +196,7 @@ router.put('/tick/config', async (req, res) => {
success: false,
error: 'Configuration validation failed',
details: validationErrors,
correlationId
correlationId,
});
}
@ -209,7 +209,7 @@ router.put('/tick/config', async (req, res) => {
return res.status(404).json({
success: false,
error: 'No active game tick configuration found',
correlationId
correlationId,
});
}
@ -222,7 +222,7 @@ router.put('/tick/config', async (req, res) => {
max_retry_attempts: max_retry_attempts || currentConfig.max_retry_attempts,
bonus_tick_threshold: bonus_tick_threshold || currentConfig.bonus_tick_threshold,
retry_delay_ms: retry_delay_ms || currentConfig.retry_delay_ms,
updated_at: new Date()
updated_at: new Date(),
})
.returning('*');
@ -236,10 +236,10 @@ router.put('/tick/config', async (req, res) => {
changes: {
before: currentConfig,
after: updatedConfig[0],
updated_by: req.user?.username
updated_by: req.user?.username,
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
user_agent: req.get('User-Agent'),
});
// Reload configuration in the service
@ -250,10 +250,10 @@ router.put('/tick/config', async (req, res) => {
message: 'Game tick configuration updated successfully',
data: {
previousConfig: currentConfig,
newConfig: updatedConfig[0]
newConfig: updatedConfig[0],
},
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
@ -261,13 +261,13 @@ router.put('/tick/config', async (req, res) => {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
stack: error.stack,
});
res.status(500).json({
success: false,
error: 'Failed to update game tick configuration',
correlationId
correlationId,
});
}
});
@ -278,7 +278,7 @@ router.put('/tick/config', async (req, res) => {
*/
router.get('/tick/logs', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
const {
page = 1,
@ -287,7 +287,7 @@ router.get('/tick/logs', async (req, res) => {
userGroup,
tickNumber,
startDate,
endDate
endDate,
} = req.query;
const pageNum = parseInt(page);
@ -300,19 +300,19 @@ router.get('/tick/logs', async (req, res) => {
if (status) {
query = query.where('status', status);
}
if (userGroup !== undefined) {
query = query.where('user_group', parseInt(userGroup));
}
if (tickNumber) {
query = query.where('tick_number', parseInt(tickNumber));
}
if (startDate) {
query = query.where('started_at', '>=', new Date(startDate));
}
if (endDate) {
query = query.where('started_at', '<=', new Date(endDate));
}
@ -341,17 +341,17 @@ router.get('/tick/logs', async (req, res) => {
errorMessage: log.error_message,
performanceMetrics: log.performance_metrics,
startedAt: log.started_at,
completedAt: log.completed_at
completedAt: log.completed_at,
})),
pagination: {
page: pageNum,
limit: limitNum,
total: parseInt(total),
pages: Math.ceil(total / limitNum)
}
pages: Math.ceil(total / limitNum),
},
},
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
@ -359,13 +359,13 @@ router.get('/tick/logs', async (req, res) => {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
stack: error.stack,
});
res.status(500).json({
success: false,
error: 'Failed to retrieve game tick logs',
correlationId
correlationId,
});
}
});
@ -376,26 +376,26 @@ router.get('/tick/logs', async (req, res) => {
*/
router.get('/performance', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
const { timeRange = '24h' } = req.query;
let interval;
switch (timeRange) {
case '1h':
interval = "1 hour";
break;
case '24h':
interval = "24 hours";
break;
case '7d':
interval = "7 days";
break;
case '30d':
interval = "30 days";
break;
default:
interval = "24 hours";
case '1h':
interval = '1 hour';
break;
case '24h':
interval = '24 hours';
break;
case '7d':
interval = '7 days';
break;
case '30d':
interval = '30 days';
break;
default:
interval = '24 hours';
}
// Get tick performance metrics
@ -406,7 +406,7 @@ router.get('/performance', async (req, res) => {
db.raw('COUNT(*) FILTER (WHERE status = \'completed\') as successful_ticks'),
db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failed_ticks'),
db.raw('AVG(processed_players) as avg_players_processed'),
db.raw('AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000) as avg_duration_ms')
db.raw('AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000) as avg_duration_ms'),
)
.where('started_at', '>=', db.raw(`NOW() - INTERVAL '${interval}'`))
.groupBy(db.raw('DATE_TRUNC(\'hour\', started_at)'))
@ -433,7 +433,7 @@ router.get('/performance', async (req, res) => {
.select(
db.raw('COUNT(*) FILTER (WHERE is_active = true) as active_players'),
db.raw('COUNT(*) FILTER (WHERE last_login >= NOW() - INTERVAL \'24 hours\') as recent_players'),
db.raw('COUNT(*) as total_players')
db.raw('COUNT(*) as total_players'),
)
.first();
@ -446,16 +446,16 @@ router.get('/performance', async (req, res) => {
totalTicks: parseInt(metric.total_ticks),
successfulTicks: parseInt(metric.successful_ticks),
failedTicks: parseInt(metric.failed_ticks),
successRate: metric.total_ticks > 0 ?
successRate: metric.total_ticks > 0 ?
((metric.successful_ticks / metric.total_ticks) * 100).toFixed(2) : 0,
avgPlayersProcessed: parseFloat(metric.avg_players_processed || 0).toFixed(1),
avgDurationMs: parseFloat(metric.avg_duration_ms || 0).toFixed(2)
avgDurationMs: parseFloat(metric.avg_duration_ms || 0).toFixed(2),
})),
databaseMetrics: dbMetrics.rows,
playerStats
playerStats,
},
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
@ -463,13 +463,13 @@ router.get('/performance', async (req, res) => {
correlationId,
adminId: req.user?.id,
error: error.message,
stack: error.stack
stack: error.stack,
});
res.status(500).json({
success: false,
error: 'Failed to retrieve performance metrics',
correlationId
correlationId,
});
}
});
@ -480,16 +480,16 @@ router.get('/performance', async (req, res) => {
*/
router.post('/tick/stop', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.warn('Admin stopping game tick service', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
adminUsername: req.user?.username,
});
gameTickService.stop();
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick',
@ -500,30 +500,30 @@ router.post('/tick/stop', async (req, res) => {
changes: {
correlation_id: correlationId,
stopped_by: req.user?.username,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
user_agent: req.get('User-Agent'),
});
res.json({
success: true,
message: 'Game tick service stopped successfully',
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
logger.error('Failed to stop game tick service', {
correlationId,
adminId: req.user?.id,
error: error.message
error: error.message,
});
res.status(500).json({
success: false,
error: 'Failed to stop game tick service',
correlationId
correlationId,
});
}
});
@ -534,16 +534,16 @@ router.post('/tick/stop', async (req, res) => {
*/
router.post('/tick/start', async (req, res) => {
const correlationId = req.correlationId || uuidv4();
try {
logger.info('Admin starting game tick service', {
correlationId,
adminId: req.user?.id,
adminUsername: req.user?.username
adminUsername: req.user?.username,
});
await gameTickService.initialize();
// Log admin action
await db('audit_log').insert({
entity_type: 'game_tick',
@ -554,10 +554,10 @@ router.post('/tick/start', async (req, res) => {
changes: {
correlation_id: correlationId,
started_by: req.user?.username,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
},
ip_address: req.ip,
user_agent: req.get('User-Agent')
user_agent: req.get('User-Agent'),
});
res.json({
@ -565,22 +565,22 @@ router.post('/tick/start', async (req, res) => {
message: 'Game tick service started successfully',
data: gameTickService.getStatus(),
timestamp: new Date().toISOString(),
correlationId
correlationId,
});
} catch (error) {
logger.error('Failed to start game tick service', {
correlationId,
adminId: req.user?.id,
error: error.message
error: error.message,
});
res.status(500).json({
success: false,
error: error.message || 'Failed to start game tick service',
correlationId
correlationId,
});
}
});
module.exports = router;
module.exports = router;

View file

@ -8,10 +8,33 @@ const router = express.Router();
// Import middleware
const { authenticatePlayer, optionalPlayerAuth, requireOwnership, injectPlayerId } = require('../middleware/auth.middleware');
const { authenticateToken } = require('../middleware/auth'); // Standardized auth
const { rateLimiters } = require('../middleware/rateLimit.middleware');
const { validators, validateRequest } = require('../middleware/validation.middleware');
const {
accountLockoutProtection,
rateLimiter,
passwordStrengthValidator,
requireEmailVerification,
sanitizeInput
} = require('../middleware/security.middleware');
const {
validateRequest: validateAuthRequest,
validateRegistrationUniqueness,
registerPlayerSchema,
loginPlayerSchema,
verifyEmailSchema,
resendVerificationSchema,
requestPasswordResetSchema,
resetPasswordSchema,
changePasswordSchema
} = require('../validators/auth.validators');
const corsMiddleware = require('../middleware/cors.middleware');
// Use standardized authentication for players
const authenticatePlayerToken = authenticateToken('player');
const optionalPlayerToken = require('../middleware/auth').optionalAuth('player');
// Import controllers
const authController = require('../controllers/api/auth.controller');
const playerController = require('../controllers/api/player.controller');
@ -54,20 +77,25 @@ const authRoutes = express.Router();
// Public authentication endpoints (with stricter rate limiting)
authRoutes.post('/register',
rateLimiters.auth,
validators.validatePlayerRegistration,
rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'registration' }),
sanitizeInput(['email', 'username']),
validateAuthRequest(registerPlayerSchema),
validateRegistrationUniqueness(),
passwordStrengthValidator('password'),
authController.register
);
authRoutes.post('/login',
rateLimiters.auth,
validators.validatePlayerLogin,
rateLimiter({ maxRequests: 5, windowMinutes: 15, action: 'login' }),
accountLockoutProtection,
sanitizeInput(['email']),
validateAuthRequest(loginPlayerSchema),
authController.login
);
// Protected authentication endpoints
authRoutes.post('/logout',
authenticatePlayer,
authenticatePlayerToken,
authController.logout
);
@ -77,33 +105,76 @@ authRoutes.post('/refresh',
);
authRoutes.get('/me',
authenticatePlayer,
authenticatePlayerToken,
authController.getProfile
);
authRoutes.put('/me',
authenticatePlayer,
authenticatePlayerToken,
requireEmailVerification,
rateLimiter({ maxRequests: 5, windowMinutes: 60, action: 'profile_update' }),
sanitizeInput(['username', 'displayName', 'bio']),
validateRequest(require('joi').object({
username: require('joi').string().alphanum().min(3).max(20).optional()
username: require('joi').string().alphanum().min(3).max(20).optional(),
displayName: require('joi').string().min(1).max(50).optional(),
bio: require('joi').string().max(500).optional()
}), 'body'),
authController.updateProfile
);
authRoutes.get('/verify',
authenticatePlayer,
authenticatePlayerToken,
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'),
authenticatePlayerToken,
rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'password_change' }),
validateAuthRequest(changePasswordSchema),
passwordStrengthValidator('newPassword'),
authController.changePassword
);
// Email verification endpoints
authRoutes.post('/verify-email',
rateLimiter({ maxRequests: 5, windowMinutes: 15, action: 'email_verification' }),
validateAuthRequest(verifyEmailSchema),
authController.verifyEmail
);
authRoutes.post('/resend-verification',
rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'resend_verification' }),
sanitizeInput(['email']),
validateAuthRequest(resendVerificationSchema),
authController.resendVerification
);
// Password reset endpoints
authRoutes.post('/request-password-reset',
rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'password_reset_request' }),
sanitizeInput(['email']),
validateAuthRequest(requestPasswordResetSchema),
authController.requestPasswordReset
);
authRoutes.post('/reset-password',
rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'password_reset' }),
validateAuthRequest(resetPasswordSchema),
passwordStrengthValidator('newPassword'),
authController.resetPassword
);
// Security utility endpoints
authRoutes.post('/check-password-strength',
rateLimiter({ maxRequests: 10, windowMinutes: 5, action: 'password_check' }),
authController.checkPasswordStrength
);
authRoutes.get('/security-status',
authenticatePlayerToken,
authController.getSecurityStatus
);
// Mount authentication routes
router.use('/auth', authRoutes);
@ -111,18 +182,18 @@ router.use('/auth', authRoutes);
* Player Management Routes
* /api/player/*
*/
const playerRoutes = express.Router();
const playerManagementRoutes = express.Router();
// All player routes require authentication
playerRoutes.use(authenticatePlayer);
playerManagementRoutes.use(authenticatePlayerToken);
playerRoutes.get('/dashboard', playerController.getDashboard);
playerManagementRoutes.get('/dashboard', playerController.getDashboard);
playerRoutes.get('/resources', playerController.getResources);
playerManagementRoutes.get('/resources', playerController.getResources);
playerRoutes.get('/stats', playerController.getStats);
playerManagementRoutes.get('/stats', playerController.getStats);
playerRoutes.put('/settings',
playerManagementRoutes.put('/settings',
validateRequest(require('joi').object({
// TODO: Define settings schema
notifications: require('joi').object({
@ -139,19 +210,19 @@ playerRoutes.put('/settings',
playerController.updateSettings
);
playerRoutes.get('/activity',
playerManagementRoutes.get('/activity',
validators.validatePagination,
playerController.getActivity
);
playerRoutes.get('/notifications',
playerManagementRoutes.get('/notifications',
validateRequest(require('joi').object({
unreadOnly: require('joi').boolean().default(false)
}), 'query'),
playerController.getNotifications
);
playerRoutes.put('/notifications/read',
playerManagementRoutes.put('/notifications/read',
validateRequest(require('joi').object({
notificationIds: require('joi').array().items(
require('joi').number().integer().positive()
@ -160,8 +231,8 @@ playerRoutes.put('/notifications/read',
playerController.markNotificationsRead
);
// Mount player routes
router.use('/player', playerRoutes);
// Mount player management routes (separate from game feature routes)
router.use('/player', playerManagementRoutes);
/**
* Combat Routes
@ -171,169 +242,25 @@ router.use('/combat', require('./api/combat'));
/**
* Game Feature Routes
* These will be expanded with actual game functionality
* Connect to existing working player route modules
*/
// 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
});
}
);
// Import existing player route modules for game features
const playerGameRoutes = require('./player');
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
});
}
);
// Mount player game routes under /player-game prefix to avoid conflicts
// These contain the actual game functionality (colonies, resources, fleets, etc.)
router.use('/player-game', playerGameRoutes);
// 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
});
}
);
// Direct mount of specific game features for convenience (these are duplicates of what's in /player/*)
// These provide direct access without the /player prefix for backwards compatibility
router.use('/colonies', authenticatePlayerToken, require('./player/colonies'));
router.use('/resources', authenticatePlayerToken, require('./player/resources'));
router.use('/fleets', authenticatePlayerToken, require('./player/fleets'));
router.use('/research', authenticatePlayerToken, require('./player/research'));
router.use('/galaxy', optionalPlayerToken, require('./player/galaxy'));
router.use('/notifications', authenticatePlayerToken, require('./player/notifications'));
router.use('/events', authenticatePlayerToken, require('./player/events'));
/**
* Error handling for API routes

View file

@ -8,29 +8,29 @@ const router = express.Router();
// Import controllers
const {
initiateCombat,
getActiveCombats,
getCombatHistory,
getCombatEncounter,
getCombatStatistics,
updateFleetPosition,
getCombatTypes,
forceResolveCombat
initiateCombat,
getActiveCombats,
getCombatHistory,
getCombatEncounter,
getCombatStatistics,
updateFleetPosition,
getCombatTypes,
forceResolveCombat,
} = require('../../controllers/api/combat.controller');
// Import middleware
const { authenticatePlayer } = require('../../middleware/auth.middleware');
const {
validateCombatInitiation,
validateFleetPositionUpdate,
validateCombatHistoryQuery,
validateParams,
checkFleetOwnership,
checkBattleAccess,
checkCombatCooldown,
checkFleetAvailability,
combatRateLimit,
logCombatAction
validateCombatInitiation,
validateFleetPositionUpdate,
validateCombatHistoryQuery,
validateParams,
checkFleetOwnership,
checkBattleAccess,
checkCombatCooldown,
checkFleetAvailability,
combatRateLimit,
logCombatAction,
} = require('../../middleware/combat.middleware');
// Apply authentication to all combat routes
@ -42,12 +42,12 @@ router.use(authenticatePlayer);
* @access Private
*/
router.post('/initiate',
logCombatAction('initiate_combat'),
combatRateLimit(5, 15), // Max 5 combat initiations per 15 minutes
checkCombatCooldown,
validateCombatInitiation,
checkFleetAvailability,
initiateCombat
logCombatAction('initiate_combat'),
combatRateLimit(5, 15), // Max 5 combat initiations per 15 minutes
checkCombatCooldown,
validateCombatInitiation,
checkFleetAvailability,
initiateCombat,
);
/**
@ -56,8 +56,8 @@ router.post('/initiate',
* @access Private
*/
router.get('/active',
logCombatAction('get_active_combats'),
getActiveCombats
logCombatAction('get_active_combats'),
getActiveCombats,
);
/**
@ -66,9 +66,9 @@ router.get('/active',
* @access Private
*/
router.get('/history',
logCombatAction('get_combat_history'),
validateCombatHistoryQuery,
getCombatHistory
logCombatAction('get_combat_history'),
validateCombatHistoryQuery,
getCombatHistory,
);
/**
@ -77,9 +77,9 @@ router.get('/history',
* @access Private
*/
router.get('/encounter/:encounterId',
logCombatAction('get_combat_encounter'),
validateParams('encounterId'),
getCombatEncounter
logCombatAction('get_combat_encounter'),
validateParams('encounterId'),
getCombatEncounter,
);
/**
@ -88,8 +88,8 @@ router.get('/encounter/:encounterId',
* @access Private
*/
router.get('/statistics',
logCombatAction('get_combat_statistics'),
getCombatStatistics
logCombatAction('get_combat_statistics'),
getCombatStatistics,
);
/**
@ -98,11 +98,11 @@ router.get('/statistics',
* @access Private
*/
router.put('/position/:fleetId',
logCombatAction('update_fleet_position'),
validateParams('fleetId'),
checkFleetOwnership,
validateFleetPositionUpdate,
updateFleetPosition
logCombatAction('update_fleet_position'),
validateParams('fleetId'),
checkFleetOwnership,
validateFleetPositionUpdate,
updateFleetPosition,
);
/**
@ -111,8 +111,8 @@ router.put('/position/:fleetId',
* @access Private
*/
router.get('/types',
logCombatAction('get_combat_types'),
getCombatTypes
logCombatAction('get_combat_types'),
getCombatTypes,
);
/**
@ -121,10 +121,10 @@ router.get('/types',
* @access Private (requires special permission)
*/
router.post('/resolve/:battleId',
logCombatAction('force_resolve_combat'),
validateParams('battleId'),
checkBattleAccess,
forceResolveCombat
logCombatAction('force_resolve_combat'),
validateParams('battleId'),
checkBattleAccess,
forceResolveCombat,
);
module.exports = router;
module.exports = router;

View file

@ -12,545 +12,545 @@ 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();
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',
colonies: '/debug/colonies',
resources: '/debug/resources',
gameEvents: '/debug/game-events'
}
});
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',
colonies: '/debug/colonies',
resources: '/debug/resources',
gameEvents: '/debug/game-events',
},
});
});
/**
* 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(`
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
});
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
});
}
} 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
});
}
try {
const redisClient = getRedisClient();
// 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
});
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();
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
});
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();
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
});
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;
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
});
// 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);
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
});
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;
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
});
}
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,
});
}
});
/**
* Colony Debug Information
*/
router.get('/colonies', async (req, res) => {
try {
const { playerId, limit = 10 } = req.query;
try {
const { playerId, limit = 10 } = req.query;
let query = db('colonies')
.select([
'colonies.*',
'planet_types.name as planet_type_name',
'galaxy_sectors.name as sector_name',
'players.username'
])
.leftJoin('planet_types', 'colonies.planet_type_id', 'planet_types.id')
.leftJoin('galaxy_sectors', 'colonies.sector_id', 'galaxy_sectors.id')
.leftJoin('players', 'colonies.player_id', 'players.id')
.orderBy('colonies.founded_at', 'desc')
.limit(parseInt(limit));
let query = db('colonies')
.select([
'colonies.*',
'planet_types.name as planet_type_name',
'galaxy_sectors.name as sector_name',
'players.username',
])
.leftJoin('planet_types', 'colonies.planet_type_id', 'planet_types.id')
.leftJoin('galaxy_sectors', 'colonies.sector_id', 'galaxy_sectors.id')
.leftJoin('players', 'colonies.player_id', 'players.id')
.orderBy('colonies.founded_at', 'desc')
.limit(parseInt(limit));
if (playerId) {
query = query.where('colonies.player_id', parseInt(playerId));
}
const colonies = await query;
// Get building counts for each colony
const coloniesWithBuildings = await Promise.all(colonies.map(async (colony) => {
const buildingCount = await db('colony_buildings')
.where('colony_id', colony.id)
.count('* as count')
.first();
const resourceProduction = await db('colony_resource_production')
.select([
'resource_types.name as resource_name',
'colony_resource_production.production_rate',
'colony_resource_production.current_stored'
])
.join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id')
.where('colony_resource_production.colony_id', colony.id)
.where('colony_resource_production.production_rate', '>', 0);
return {
...colony,
buildingCount: parseInt(buildingCount.count) || 0,
resourceProduction
};
}));
res.json({
colonies: coloniesWithBuildings,
totalCount: coloniesWithBuildings.length,
filters: { playerId, limit },
correlationId: req.correlationId
});
} catch (error) {
logger.error('Colony debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
if (playerId) {
query = query.where('colonies.player_id', parseInt(playerId));
}
const colonies = await query;
// Get building counts for each colony
const coloniesWithBuildings = await Promise.all(colonies.map(async (colony) => {
const buildingCount = await db('colony_buildings')
.where('colony_id', colony.id)
.count('* as count')
.first();
const resourceProduction = await db('colony_resource_production')
.select([
'resource_types.name as resource_name',
'colony_resource_production.production_rate',
'colony_resource_production.current_stored',
])
.join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id')
.where('colony_resource_production.colony_id', colony.id)
.where('colony_resource_production.production_rate', '>', 0);
return {
...colony,
buildingCount: parseInt(buildingCount.count) || 0,
resourceProduction,
};
}));
res.json({
colonies: coloniesWithBuildings,
totalCount: coloniesWithBuildings.length,
filters: { playerId, limit },
correlationId: req.correlationId,
});
} catch (error) {
logger.error('Colony debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId,
});
}
});
/**
* Resource Debug Information
*/
router.get('/resources', async (req, res) => {
try {
const { playerId } = req.query;
try {
const { playerId } = req.query;
// Get resource types
const resourceTypes = await db('resource_types')
.where('is_active', true)
.orderBy('category')
.orderBy('name');
// Get resource types
const resourceTypes = await db('resource_types')
.where('is_active', true)
.orderBy('category')
.orderBy('name');
let resourceSummary = {};
const resourceSummary = {};
if (playerId) {
// Get specific player resources
const playerResources = await db('player_resources')
.select([
'player_resources.*',
'resource_types.name as resource_name',
'resource_types.category'
])
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.where('player_resources.player_id', parseInt(playerId));
if (playerId) {
// Get specific player resources
const playerResources = await db('player_resources')
.select([
'player_resources.*',
'resource_types.name as resource_name',
'resource_types.category',
])
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.where('player_resources.player_id', parseInt(playerId));
resourceSummary.playerResources = playerResources;
resourceSummary.playerResources = playerResources;
// Get player's colony resource production
const colonyProduction = await db('colony_resource_production')
.select([
'colonies.name as colony_name',
'resource_types.name as resource_name',
'colony_resource_production.production_rate',
'colony_resource_production.current_stored'
])
.join('colonies', 'colony_resource_production.colony_id', 'colonies.id')
.join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id')
.where('colonies.player_id', parseInt(playerId))
.where('colony_resource_production.production_rate', '>', 0);
// Get player's colony resource production
const colonyProduction = await db('colony_resource_production')
.select([
'colonies.name as colony_name',
'resource_types.name as resource_name',
'colony_resource_production.production_rate',
'colony_resource_production.current_stored',
])
.join('colonies', 'colony_resource_production.colony_id', 'colonies.id')
.join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id')
.where('colonies.player_id', parseInt(playerId))
.where('colony_resource_production.production_rate', '>', 0);
resourceSummary.colonyProduction = colonyProduction;
} else {
// Get global resource statistics
const totalResources = await db('player_resources')
.select([
'resource_types.name as resource_name',
db.raw('SUM(player_resources.amount) as total_amount'),
db.raw('COUNT(player_resources.id) as player_count'),
db.raw('AVG(player_resources.amount) as average_amount')
])
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.groupBy('resource_types.id', 'resource_types.name')
.orderBy('resource_types.name');
resourceSummary.colonyProduction = colonyProduction;
} else {
// Get global resource statistics
const totalResources = await db('player_resources')
.select([
'resource_types.name as resource_name',
db.raw('SUM(player_resources.amount) as total_amount'),
db.raw('COUNT(player_resources.id) as player_count'),
db.raw('AVG(player_resources.amount) as average_amount'),
])
.join('resource_types', 'player_resources.resource_type_id', 'resource_types.id')
.groupBy('resource_types.id', 'resource_types.name')
.orderBy('resource_types.name');
resourceSummary.globalStats = totalResources;
}
res.json({
resourceTypes,
...resourceSummary,
filters: { playerId },
correlationId: req.correlationId
});
} catch (error) {
logger.error('Resource debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
resourceSummary.globalStats = totalResources;
}
res.json({
resourceTypes,
...resourceSummary,
filters: { playerId },
correlationId: req.correlationId,
});
} catch (error) {
logger.error('Resource debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId,
});
}
});
/**
* Game Events Debug Information
*/
router.get('/game-events', (req, res) => {
try {
const serviceLocator = require('../services/ServiceLocator');
const gameEventService = serviceLocator.get('gameEventService');
try {
const serviceLocator = require('../services/ServiceLocator');
const gameEventService = serviceLocator.get('gameEventService');
if (!gameEventService) {
return res.json({
status: 'not_available',
message: 'Game event service not initialized',
correlationId: req.correlationId
});
}
const connectedPlayers = gameEventService.getConnectedPlayerCount();
// Get room information
const io = gameEventService.io;
const rooms = Array.from(io.sockets.adapter.rooms.entries()).map(([roomName, socketSet]) => ({
name: roomName,
socketCount: socketSet.size,
type: roomName.includes(':') ? roomName.split(':')[0] : 'unknown'
}));
res.json({
status: 'active',
connectedPlayers,
rooms: {
total: rooms.length,
breakdown: rooms
},
eventTypes: [
'colony_created',
'building_constructed',
'resources_updated',
'resource_production',
'colony_status_update',
'error',
'notification',
'player_status_change',
'system_announcement'
],
correlationId: req.correlationId
});
} catch (error) {
logger.error('Game events debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
if (!gameEventService) {
return res.json({
status: 'not_available',
message: 'Game event service not initialized',
correlationId: req.correlationId,
});
}
const connectedPlayers = gameEventService.getConnectedPlayerCount();
// Get room information
const io = gameEventService.io;
const rooms = Array.from(io.sockets.adapter.rooms.entries()).map(([roomName, socketSet]) => ({
name: roomName,
socketCount: socketSet.size,
type: roomName.includes(':') ? roomName.split(':')[0] : 'unknown',
}));
res.json({
status: 'active',
connectedPlayers,
rooms: {
total: rooms.length,
breakdown: rooms,
},
eventTypes: [
'colony_created',
'building_constructed',
'resources_updated',
'resource_production',
'colony_status_update',
'error',
'notification',
'player_status_change',
'system_announcement',
],
correlationId: req.correlationId,
});
} catch (error) {
logger.error('Game events debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId,
});
}
});
/**
* Add resources to a player (for testing)
*/
router.post('/add-resources', async (req, res) => {
try {
const { playerId, resources } = req.body;
try {
const { playerId, resources } = req.body;
if (!playerId || !resources) {
return res.status(400).json({
error: 'playerId and resources are required',
correlationId: req.correlationId
});
}
const serviceLocator = require('../services/ServiceLocator');
const ResourceService = require('../services/resource/ResourceService');
const gameEventService = serviceLocator.get('gameEventService');
const resourceService = new ResourceService(gameEventService);
const updatedResources = await resourceService.addPlayerResources(
playerId,
resources,
req.correlationId
);
res.json({
success: true,
message: 'Resources added successfully',
playerId,
addedResources: resources,
updatedResources,
correlationId: req.correlationId
});
} catch (error) {
logger.error('Add resources debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId
});
if (!playerId || !resources) {
return res.status(400).json({
error: 'playerId and resources are required',
correlationId: req.correlationId,
});
}
const serviceLocator = require('../services/ServiceLocator');
const ResourceService = require('../services/resource/ResourceService');
const gameEventService = serviceLocator.get('gameEventService');
const resourceService = new ResourceService(gameEventService);
const updatedResources = await resourceService.addPlayerResources(
playerId,
resources,
req.correlationId,
);
res.json({
success: true,
message: 'Resources added successfully',
playerId,
addedResources: resources,
updatedResources,
correlationId: req.correlationId,
});
} catch (error) {
logger.error('Add resources debug error:', error);
res.status(500).json({
error: error.message,
correlationId: req.correlationId,
});
}
});
module.exports = router;
module.exports = router;

View file

@ -16,111 +16,111 @@ 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
};
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);
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'
}
});
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'
});
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'
});
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
@ -128,17 +128,17 @@ 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');
router.use('/api/admin', adminRoutes);
logger.info('Admin routes enabled');
} else {
logger.info('Admin routes disabled');
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');
const debugRoutes = require('./debug');
router.use('/debug', debugRoutes);
logger.info('Debug routes enabled');
}
module.exports = router;
module.exports = router;

View file

@ -64,4 +64,4 @@ router.post('/reset-password', asyncHandler(async (req, res) => {
});
}));
module.exports = router;
module.exports = router;

View file

@ -7,42 +7,42 @@ const express = require('express');
const router = express.Router();
const {
createColony,
getPlayerColonies,
getColonyDetails,
constructBuilding,
getBuildingTypes,
getPlanetTypes,
getGalaxySectors
createColony,
getPlayerColonies,
getColonyDetails,
constructBuilding,
getBuildingTypes,
getPlanetTypes,
getGalaxySectors,
} = require('../../controllers/player/colony.controller');
const { validateRequest } = require('../../middleware/validation.middleware');
const {
createColonySchema,
constructBuildingSchema,
colonyIdParamSchema
createColonySchema,
constructBuildingSchema,
colonyIdParamSchema,
} = require('../../validators/colony.validators');
// Colony CRUD operations
router.post('/',
validateRequest(createColonySchema),
createColony
router.post('/',
validateRequest(createColonySchema),
createColony,
);
router.get('/',
getPlayerColonies
router.get('/',
getPlayerColonies,
);
router.get('/:colonyId',
validateRequest(colonyIdParamSchema, 'params'),
getColonyDetails
router.get('/:colonyId',
validateRequest(colonyIdParamSchema, 'params'),
getColonyDetails,
);
// Building operations
router.post('/:colonyId/buildings',
validateRequest(colonyIdParamSchema, 'params'),
validateRequest(constructBuildingSchema),
constructBuilding
router.post('/:colonyId/buildings',
validateRequest(colonyIdParamSchema, 'params'),
validateRequest(constructBuildingSchema),
constructBuilding,
);
// Reference data endpoints
@ -50,4 +50,4 @@ router.get('/ref/building-types', getBuildingTypes);
router.get('/ref/planet-types', getPlanetTypes);
router.get('/ref/galaxy-sectors', getGalaxySectors);
module.exports = router;
module.exports = router;

View file

@ -0,0 +1,33 @@
/**
* Player Events Routes
* Handles player event history and notifications
*/
const express = require('express');
const router = express.Router();
// TODO: Implement events routes
router.get('/', (req, res) => {
res.json({
message: 'Events routes not yet implemented',
available_endpoints: {
'/history': 'Get event history',
'/recent': 'Get recent events',
'/unread': 'Get unread events'
}
});
});
router.get('/history', (req, res) => {
res.json({ message: 'Event history endpoint not implemented' });
});
router.get('/recent', (req, res) => {
res.json({ message: 'Recent events endpoint not implemented' });
});
router.get('/unread', (req, res) => {
res.json({ message: 'Unread events endpoint not implemented' });
});
module.exports = router;

View file

@ -0,0 +1,36 @@
/**
* Player Fleet Routes
* Handles fleet management and operations
*/
const express = require('express');
const router = express.Router();
const fleetController = require('../../controllers/api/fleet.controller');
// Fleet management routes
router.get('/', fleetController.getPlayerFleets);
router.post('/', fleetController.createFleet);
router.get('/:fleetId', fleetController.getFleetDetails);
router.delete('/:fleetId', fleetController.disbandFleet);
// Fleet operations
router.post('/:fleetId/move', fleetController.moveFleet);
// TODO: Combat operations (will be implemented when combat system is enhanced)
router.post('/:fleetId/attack', (req, res) => {
res.status(501).json({
success: false,
error: 'Not implemented',
message: 'Fleet combat operations will be available in a future update'
});
});
// Ship design routes
router.get('/ship-designs/classes', fleetController.getShipClassesInfo);
router.get('/ship-designs/:designId', fleetController.getShipDesignDetails);
router.get('/ship-designs', fleetController.getAvailableShipDesigns);
// Ship construction validation
router.post('/validate-construction', fleetController.validateShipConstruction);
module.exports = router;

View file

@ -0,0 +1,33 @@
/**
* Player Galaxy Routes
* Handles galaxy exploration and sector viewing
*/
const express = require('express');
const router = express.Router();
// TODO: Implement galaxy routes
router.get('/', (req, res) => {
res.json({
message: 'Galaxy routes not yet implemented',
available_endpoints: {
'/sectors': 'List galaxy sectors',
'/explore': 'Explore new areas',
'/map': 'View galaxy map'
}
});
});
router.get('/sectors', (req, res) => {
res.json({ message: 'Galaxy sectors endpoint not implemented' });
});
router.get('/explore', (req, res) => {
res.json({ message: 'Galaxy exploration endpoint not implemented' });
});
router.get('/map', (req, res) => {
res.json({ message: 'Galaxy map endpoint not implemented' });
});
module.exports = router;

View file

@ -47,4 +47,4 @@ router.get('/status', authenticateToken('player'), asyncHandler(async (req, res)
});
}));
module.exports = router;
module.exports = router;

View file

@ -0,0 +1,33 @@
/**
* Player Notifications Routes
* Handles player notifications and messages
*/
const express = require('express');
const router = express.Router();
// TODO: Implement notifications routes
router.get('/', (req, res) => {
res.json({
message: 'Notifications routes not yet implemented',
available_endpoints: {
'/unread': 'Get unread notifications',
'/all': 'Get all notifications',
'/mark-read': 'Mark notifications as read'
}
});
});
router.get('/unread', (req, res) => {
res.json({ message: 'Unread notifications endpoint not implemented' });
});
router.get('/all', (req, res) => {
res.json({ message: 'All notifications endpoint not implemented' });
});
router.post('/mark-read', (req, res) => {
res.json({ message: 'Mark notifications read endpoint not implemented' });
});
module.exports = router;

View file

@ -0,0 +1,33 @@
/**
* Player Profile Routes
* Handles player profile management
*/
const express = require('express');
const router = express.Router();
// TODO: Implement profile routes
router.get('/', (req, res) => {
res.json({
message: 'Profile routes not yet implemented',
available_endpoints: {
'/': 'Get player profile',
'/update': 'Update player profile',
'/settings': 'Get/update player settings'
}
});
});
router.put('/', (req, res) => {
res.json({ message: 'Profile update endpoint not implemented' });
});
router.get('/settings', (req, res) => {
res.json({ message: 'Profile settings endpoint not implemented' });
});
router.put('/settings', (req, res) => {
res.json({ message: 'Profile settings update endpoint not implemented' });
});
module.exports = router;

View file

@ -0,0 +1,67 @@
/**
* Player Research Routes
* Handles research and technology management
*/
const express = require('express');
const router = express.Router();
// Import controllers and middleware
const researchController = require('../../controllers/api/research.controller');
const {
validateStartResearch,
validateTechnologyTreeFilter,
validateResearchStats
} = require('../../validators/research.validators');
/**
* Get current research status for the authenticated player
* GET /player/research/
*/
router.get('/', researchController.getResearchStatus);
/**
* Get available technologies for research
* GET /player/research/available
*/
router.get('/available', researchController.getAvailableTechnologies);
/**
* Get completed technologies
* GET /player/research/completed
*/
router.get('/completed', researchController.getCompletedTechnologies);
/**
* Get full technology tree with player progress
* GET /player/research/technology-tree
* Query params: category, tier, status, include_unavailable, sort_by, sort_order
*/
router.get('/technology-tree',
validateTechnologyTreeFilter,
researchController.getTechnologyTree
);
/**
* Get research queue (current and queued research)
* GET /player/research/queue
*/
router.get('/queue', researchController.getResearchQueue);
/**
* Start research on a technology
* POST /player/research/start
* Body: { technology_id: number }
*/
router.post('/start',
validateStartResearch,
researchController.startResearch
);
/**
* Cancel current research
* POST /player/research/cancel
*/
router.post('/cancel', researchController.cancelResearch);
module.exports = router;

View file

@ -7,48 +7,48 @@ const express = require('express');
const router = express.Router();
const {
getPlayerResources,
getPlayerResourceSummary,
getResourceProduction,
addResources,
transferResources,
getResourceTypes
getPlayerResources,
getPlayerResourceSummary,
getResourceProduction,
addResources,
transferResources,
getResourceTypes,
} = require('../../controllers/player/resource.controller');
const { validateRequest } = require('../../middleware/validation.middleware');
const {
transferResourcesSchema,
addResourcesSchema,
resourceQuerySchema
transferResourcesSchema,
addResourcesSchema,
resourceQuerySchema,
} = require('../../validators/resource.validators');
// Resource information endpoints
router.get('/',
validateRequest(resourceQuerySchema, 'query'),
getPlayerResources
router.get('/',
validateRequest(resourceQuerySchema, 'query'),
getPlayerResources,
);
router.get('/summary',
getPlayerResourceSummary
router.get('/summary',
getPlayerResourceSummary,
);
router.get('/production',
getResourceProduction
router.get('/production',
getResourceProduction,
);
// Resource manipulation endpoints
router.post('/transfer',
validateRequest(transferResourcesSchema),
transferResources
router.post('/transfer',
validateRequest(transferResourcesSchema),
transferResources,
);
// Development/testing endpoints
router.post('/add',
validateRequest(addResourcesSchema),
addResources
router.post('/add',
validateRequest(addResourcesSchema),
addResources,
);
// Reference data endpoints
router.get('/types', getResourceTypes);
module.exports = router;
module.exports = router;