- Updated OAuth endpoints for Challonge and Discord in platforms configuration. - Implemented session and CSRF cookie initialization in main application entry. - Enhanced Challonge API client to avoid sending sensitive API keys from the browser. - Modified tournament querying to handle new state definitions and improved error handling. - Updated UI components to reflect server-side storage of authentication tokens. - Improved user experience in API Key Manager and Authentication Hub with clearer messaging. - Refactored client credentials management to support asynchronous operations. - Adjusted API client tests to validate new request configurations. - Updated Vite configuration to support session and CSRF handling through proxies.
269 lines
7.4 KiB
JavaScript
269 lines
7.4 KiB
JavaScript
/**
|
||
* Environment Variable Validation
|
||
*
|
||
* Validates required environment variables at startup and provides
|
||
* helpful error messages for production deployments.
|
||
*/
|
||
|
||
/**
|
||
* Required environment variables for production
|
||
*/
|
||
const REQUIRED_ENV_VARS = {
|
||
// Deployment Configuration
|
||
DEPLOYMENT_TARGET: {
|
||
required: true,
|
||
description: 'Deployment environment (dev, docker-local, production)',
|
||
validate: val => ['dev', 'docker-local', 'production'].includes(val)
|
||
},
|
||
|
||
// Server Configuration
|
||
NODE_ENV: {
|
||
required: true,
|
||
description: 'Environment mode (development, production)',
|
||
validate: val => ['development', 'production', 'test'].includes(val)
|
||
},
|
||
PORT: {
|
||
required: true,
|
||
description: 'Server port number',
|
||
validate: val =>
|
||
!isNaN(parseInt(val)) && parseInt(val) > 0 && parseInt(val) < 65536
|
||
},
|
||
|
||
// Frontend URL for CORS
|
||
FRONTEND_URL: {
|
||
required: true,
|
||
description: 'Frontend URL for CORS',
|
||
validate: (val, env) => {
|
||
if (!val) return false;
|
||
|
||
// Validate that FRONTEND_URL matches DEPLOYMENT_TARGET (if set)
|
||
const target = env?.DEPLOYMENT_TARGET;
|
||
if (!target) return true; // Skip validation if target not set yet
|
||
|
||
if (target === 'dev' && !val.includes('localhost:5173')) {
|
||
console.error(
|
||
'⚠️ FRONTEND_URL should be http://localhost:5173 for dev target'
|
||
);
|
||
return false;
|
||
}
|
||
if (target === 'docker-local' && !val.includes('localhost:8099')) {
|
||
console.error(
|
||
'⚠️ FRONTEND_URL should be http://localhost:8099 for docker-local target'
|
||
);
|
||
return false;
|
||
}
|
||
if (target === 'production' && !val.includes('app.pokedex.online')) {
|
||
console.error(
|
||
'⚠️ FRONTEND_URL should be https://app.pokedex.online for production target'
|
||
);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
},
|
||
|
||
// Optional but recommended for production
|
||
SESSION_SECRET: {
|
||
required: false,
|
||
description: 'Secret key for session encryption',
|
||
warn: val =>
|
||
!val || val.length < 32
|
||
? 'SESSION_SECRET should be at least 32 characters for security'
|
||
: null
|
||
},
|
||
|
||
// Token encryption key (required for server-side OAuth token storage in production)
|
||
OAUTH_TOKEN_ENC_KEY: {
|
||
required: false,
|
||
description:
|
||
'Base64-encoded 32-byte key for encrypting OAuth tokens at rest (AES-256-GCM)',
|
||
validate: (val, env) => {
|
||
const target = env?.DEPLOYMENT_TARGET;
|
||
if (target !== 'production') return true;
|
||
if (!val) {
|
||
console.error(
|
||
'❌ OAUTH_TOKEN_ENC_KEY is required in production to encrypt OAuth tokens'
|
||
);
|
||
return false;
|
||
}
|
||
// Best-effort validation: base64 decode should yield 32 bytes
|
||
try {
|
||
const buf = Buffer.from(val, 'base64');
|
||
return buf.length === 32;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
},
|
||
|
||
// Admin auth
|
||
ADMIN_PASSWORD: {
|
||
required: false,
|
||
description: 'Admin password for /auth/login (recommended for production)'
|
||
},
|
||
|
||
// Challonge OAuth (optional)
|
||
CHALLONGE_CLIENT_ID: {
|
||
required: false,
|
||
description: 'Challonge OAuth client ID'
|
||
},
|
||
CHALLONGE_CLIENT_SECRET: {
|
||
required: false,
|
||
description: 'Challonge OAuth client secret'
|
||
},
|
||
CHALLONGE_REDIRECT_URI: {
|
||
required: false,
|
||
description: 'OAuth redirect URI'
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Validate environment variables
|
||
* @returns {Object} Validation result with errors and warnings
|
||
*/
|
||
export function validateEnvironment() {
|
||
const errors = [];
|
||
const warnings = [];
|
||
const missing = [];
|
||
|
||
// Check required variables
|
||
for (const [key, config] of Object.entries(REQUIRED_ENV_VARS)) {
|
||
const value = process.env[key];
|
||
|
||
// Check if required variable is missing
|
||
if (config.required && !value) {
|
||
errors.push(
|
||
`Missing required environment variable: ${key} - ${config.description}`
|
||
);
|
||
missing.push(key);
|
||
continue;
|
||
}
|
||
|
||
// Validate value if present
|
||
if (value && config.validate && !config.validate(value)) {
|
||
errors.push(
|
||
`Invalid value for ${key}: "${value}" - ${config.description}`
|
||
);
|
||
}
|
||
|
||
// Check for warnings
|
||
if (config.warn) {
|
||
const warning = config.warn(value, process.env);
|
||
if (warning) {
|
||
warnings.push(`${key}: ${warning}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
errors,
|
||
warnings,
|
||
missing
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Validate environment and exit if critical errors found
|
||
* @param {boolean} exitOnError - Whether to exit process on validation errors (default: true)
|
||
*/
|
||
export function validateOrExit(exitOnError = true) {
|
||
const result = validateEnvironment();
|
||
|
||
// Print validation results
|
||
console.log('\n🔍 Environment Validation:');
|
||
console.log(
|
||
` DEPLOYMENT_TARGET: ${process.env.DEPLOYMENT_TARGET || 'not set'}`
|
||
);
|
||
console.log(` NODE_ENV: ${process.env.NODE_ENV || 'not set'}`);
|
||
console.log(` PORT: ${process.env.PORT || 'not set'}`);
|
||
console.log(` FRONTEND_URL: ${process.env.FRONTEND_URL || 'not set'}`);
|
||
|
||
// Show errors
|
||
if (result.errors.length > 0) {
|
||
console.error('\n❌ Environment Validation Errors:');
|
||
result.errors.forEach(error => console.error(` - ${error}`));
|
||
|
||
if (result.missing.length > 0) {
|
||
console.error('\n💡 Tip: Create a .env file with these variables:');
|
||
result.missing.forEach(key => {
|
||
console.error(` ${key}=your_value_here`);
|
||
});
|
||
console.error('\n See .env.example for reference');
|
||
}
|
||
|
||
if (exitOnError) {
|
||
console.error('\n❌ Server cannot start due to environment errors\n');
|
||
process.exit(1);
|
||
}
|
||
} else {
|
||
console.log(' ✅ All required variables present');
|
||
}
|
||
|
||
// Show warnings
|
||
if (result.warnings.length > 0) {
|
||
console.warn('\n⚠️ Environment Warnings:');
|
||
result.warnings.forEach(warning => console.warn(` - ${warning}`));
|
||
}
|
||
|
||
console.log('');
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Get configuration object with validated environment variables
|
||
* @returns {Object} Configuration object
|
||
*/
|
||
export function getConfig() {
|
||
const deploymentTarget = process.env.DEPLOYMENT_TARGET || 'dev';
|
||
const frontendUrl = process.env.FRONTEND_URL;
|
||
|
||
return {
|
||
deploymentTarget,
|
||
nodeEnv: process.env.NODE_ENV || 'development',
|
||
port: parseInt(process.env.PORT || '3001'),
|
||
isProduction: process.env.NODE_ENV === 'production',
|
||
isDevelopment: process.env.NODE_ENV === 'development',
|
||
|
||
// Challonge OAuth
|
||
challonge: {
|
||
clientId: process.env.CHALLONGE_CLIENT_ID,
|
||
clientSecret: process.env.CHALLONGE_CLIENT_SECRET,
|
||
redirectUri: process.env.CHALLONGE_REDIRECT_URI,
|
||
configured: !!(
|
||
process.env.CHALLONGE_CLIENT_ID && process.env.CHALLONGE_CLIENT_SECRET
|
||
)
|
||
},
|
||
|
||
// CORS - Single origin based on deployment target
|
||
cors: {
|
||
origin: frontendUrl,
|
||
credentials: true
|
||
},
|
||
|
||
// Security
|
||
session: {
|
||
secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production'
|
||
},
|
||
|
||
// Admin auth (JWT secret uses session secret for now)
|
||
secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
|
||
adminPassword: process.env.ADMIN_PASSWORD,
|
||
|
||
// Discord User Permissions
|
||
discord: {
|
||
adminUsers: process.env.DISCORD_ADMIN_USERS
|
||
? process.env.DISCORD_ADMIN_USERS.split(',').map(u =>
|
||
u.trim().toLowerCase()
|
||
)
|
||
: []
|
||
}
|
||
};
|
||
}
|
||
|
||
// Run validation when executed directly
|
||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||
validateOrExit();
|
||
}
|