Jan 2025 Update

Team Search Improvements:
Added required game selection before team name input
Added VR Battles image as the main embed image
Removed buttons for cleaner display
Added sorting by game mode (Squads > Duo > Solo)
Made team display more compact with icons and shortened stats
Added SQL injection protection and input sanitization
User Search Improvements:
Made game selection required before username input
Added VR Battles image as the main embed image
Added better user status messages:
played
Security Enhancements:
Added input validation and sanitization at multiple levels
Limited input lengths to prevent buffer overflow
Added proper error handling and logging
Implemented safe API calls with timeouts and validation
Added protection against SQL injection
Code Organization:
Improved error messages for better user feedback
Added comprehensive logging for monitoring
Made responses visible to everyone in the channel
Cleaned up code structure and removed redundant parts
Development Environment:
Set up proper development configuration
Added environment variable management
Improved command deployment process
This commit is contained in:
VinceC
2025-01-04 08:59:51 -06:00
parent 5a836a537f
commit 7e4354e37b
10 changed files with 1104 additions and 132 deletions

View File

@@ -151,10 +151,40 @@ class CommandHandler {
const username = interaction.options.getString('username');
const gameFilter = interaction.options.getString('game');
const userData = await this.playerService.findUserByUsername(username);
// Input validation
if (!username || typeof username !== 'string') {
await interaction.editReply({
content: '❌ Invalid username provided.'
});
return;
}
// Enhanced sanitization
const sanitizedUsername = username
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
.trim()
.slice(0, 100);
if (!sanitizedUsername) {
await interaction.editReply({
content: '❌ Username must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).'
});
return;
}
// Log sanitized input for monitoring
this.logger.debug('User search input:', {
original: username,
sanitized: sanitizedUsername,
game: gameFilter,
userId: interaction.user.id,
timestamp: new Date().toISOString()
});
const userData = await this.playerService.findUserByUsername(sanitizedUsername);
if (!userData || !userData.success) {
await interaction.editReply({
content: '❌ User not found or an error occurred while fetching data.',
content: '❌ User not found or an error occurred while fetching data.'
});
return;
}
@@ -163,20 +193,43 @@ class CommandHandler {
? JSON.parse(userData.player_data)
: userData.player_data;
// Check if the user is on a team for the specified game
const isOnTeam = playerData.teams && Object.values(playerData.teams)
.some(team => team.game_name === gameFilter);
// Check if the user has played the specified game
const hasPlayedGame = playerData.stats?.games &&
Object.keys(playerData.stats.games)
.some(game => game.toLowerCase() === gameFilter.toLowerCase());
if (!hasPlayedGame) {
const teamMessage = isOnTeam ?
`\nThey are on a team for ${gameFilter} but haven't played any matches yet.` :
'';
await interaction.editReply({
content: `✅ User found, but they haven't played ${gameFilter} yet.${teamMessage}`
});
return;
}
const embed = EmbedBuilders.createUserEmbed(playerData, gameFilter, this.playerService);
const row = this.createActionRow(playerData.username);
await interaction.editReply({
embeds: [embed],
components: [row],
components: [row]
});
} catch (error) {
this.logger.error('Error in handleFindUser:', {
error: error.message,
username: interaction.options.getString('username'),
game: interaction.options.getString('game'),
userId: interaction.user.id,
timestamp: new Date().toISOString()
});
throw error;
await interaction.editReply({
content: '❌ An error occurred while searching for the user.'
});
}
}
@@ -225,47 +278,64 @@ class CommandHandler {
const teamName = interaction.options.getString('teamname');
const gameFilter = interaction.options.getString('game');
const teamData = await this.playerService.findTeamByName(teamName, gameFilter);
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
// Input validation
if (!teamName || typeof teamName !== 'string') {
await interaction.editReply({
content: '❌ No teams found matching your search criteria.',
content: '❌ Invalid team name provided.'
});
return;
}
const embed = EmbedBuilders.createTeamEmbed(teamData.teams);
// Enhanced sanitization - only allow alphanumeric characters, spaces, and specific symbols
// Limit the length to prevent buffer overflow attacks
const sanitizedTeamName = teamName
.replace(/[^a-zA-Z0-9\s\-_.]/g, '') // Remove any characters that aren't alphanumeric, space, hyphen, underscore, or period
.trim()
.slice(0, 100); // Limit length to 100 characters
// Create buttons for each team
const rows = teamData.teams.slice(0, 5).map(team => {
return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel(`🔵 View ${team.name} (${team.game_mode})`)
.setStyle(ButtonStyle.Link)
.setURL(`${this.playerService.baseUrl}/teams/${team.id}`),
);
if (!sanitizedTeamName) {
await interaction.editReply({
content: '❌ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).'
});
return;
}
// Log sanitized input for monitoring
this.logger.debug('Team search input:', {
original: teamName,
sanitized: sanitizedTeamName,
game: gameFilter,
userId: interaction.user.id,
timestamp: new Date().toISOString()
});
// Add Discord join button in a separate row
rows.push(
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel('🟡 Join Discord')
.setStyle(ButtonStyle.Link)
.setURL('https://discord.gg/j3DKVATPGQ')
)
);
// Game filter is pre-validated by Discord's slash command choices
const teamData = await this.playerService.findTeamByName(sanitizedTeamName, gameFilter);
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
await interaction.editReply({
content: '❌ No teams found matching your search criteria.'
});
return;
}
const embeds = EmbedBuilders.createTeamEmbed(teamData.teams);
await interaction.editReply({
embeds: [embed],
components: rows,
embeds: embeds,
components: []
});
} catch (error) {
this.logger.error('Error in handleFindTeam:', {
error: error.message,
teamname: interaction.options.getString('teamname'),
teamName: interaction.options.getString('teamname'),
game: interaction.options.getString('game'),
userId: interaction.user.id,
timestamp: new Date().toISOString()
});
throw error;
await interaction.editReply({
content: '❌ An error occurred while searching for teams.'
});
}
}

View File

@@ -70,35 +70,59 @@ class PlayerService {
return `${this.baseUrl}/profile/${username}/stats`;
}
async findTeamByName(teamName, gameFilter = null) {
async findTeamByName(teamName, gameFilter) {
try {
console.log(`Fetching team data for: ${teamName}${gameFilter ? ` in ${gameFilter}` : ''}`);
const url = `${this.baseUrl}/api/get_team_data_by_name/${encodeURIComponent(teamName)}`;
console.log(`API URL: ${url}`);
const response = await axios.get(url, {
timeout: 5000
});
console.log('API Response:', JSON.stringify(response.data, null, 2));
if (response.data && response.data.success) {
// Filter teams by game if gameFilter is provided
if (gameFilter && response.data.teams) {
response.data.teams = response.data.teams.filter(
team => team.game_name.toLowerCase() === gameFilter.toLowerCase()
);
}
// Double-check sanitization here as well for defense in depth
if (!teamName || typeof teamName !== 'string') {
throw new Error('Invalid team name provided');
}
// Additional sanitization at the service level
const sanitizedTeamName = teamName
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
.trim()
.slice(0, 100);
if (!sanitizedTeamName) {
throw new Error('Invalid team name after sanitization');
}
// Use URL encoding for the query parameters
const encodedTeamName = encodeURIComponent(sanitizedTeamName);
const url = `${this.baseUrl}/api/get_team_data_by_name/${encodedTeamName}`;
const response = await axios.get(url, {
timeout: 5000, // 5 second timeout
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
validateStatus: function (status) {
return status >= 200 && status < 300; // Only accept success status codes
}
});
// Validate response structure
if (!response.data || typeof response.data !== 'object') {
throw new Error('Invalid response format from API');
}
// If game filter is provided, filter the teams
if (gameFilter && response.data.teams) {
response.data.teams = response.data.teams.filter(
team => team.game_name === gameFilter
);
}
return response.data;
} catch (error) {
console.error('Error fetching team data:', {
message: error.message,
response: error.response?.data,
status: error.response?.status
this.logger.error('Error in findTeamByName:', {
error: error.message,
teamName,
gameFilter,
timestamp: new Date().toISOString()
});
return null;
return { success: false, error: 'Failed to fetch team data' };
}
}
}

View File

@@ -6,7 +6,8 @@ class EmbedBuilders {
const embed = new EmbedBuilder()
.setTitle(`User: ${playerData.username || 'Unknown'}`)
.setDescription(profile.bio || 'No bio available')
.setColor('#0099ff');
.setColor('#0099ff')
.setImage('https://www.vrbattles.gg/assets/images/Qubi/vrwearcubesatad.png');
// Add thumbnail (avatar)
if (profile.avatar) {
@@ -15,12 +16,6 @@ class EmbedBuilders {
);
}
// Add badge image using PlayerService
if (profile.current_level_badge) {
const badgeImageUrl = playerService.getBadgeImageUrl(profile.current_level_badge);
embed.setImage(badgeImageUrl);
}
// Add profile fields
const profileFields = [];
if (profile.country) profileFields.push({ name: 'Country', value: profile.country, inline: true });
@@ -210,73 +205,71 @@ class EmbedBuilders {
}
static createTeamEmbed(teams) {
const embed = new EmbedBuilder()
const embeds = [];
const infoEmbed = new EmbedBuilder()
.setTitle(`Teams Found`)
.setColor('#0099ff');
.setColor('#0099ff')
.setImage('https://www.vrbattles.gg/assets/images/Qubi/vrwearcubesatad.png');
if (!teams || teams.length === 0) {
embed.setDescription('No teams found');
return embed;
infoEmbed.setDescription('No teams found');
return [infoEmbed];
}
// Sort teams by date created (newest first)
teams.sort((a, b) => new Date(b.date_created) - new Date(a.date_created));
// Sort teams by game mode priority and date
const gameModeOrder = {
'Squads': 1,
'Duo': 2,
'Solo': 3
};
teams.sort((a, b) => {
const modeA = gameModeOrder[a.game_mode] || 999;
const modeB = gameModeOrder[b.game_mode] || 999;
if (modeA !== modeB) return modeA - modeB;
return new Date(b.date_created) - new Date(a.date_created);
});
teams.forEach((team, index) => {
// Add team header
embed.addFields({
name: `${index + 1}. ${team.name} - ${team.game_name} (${team.game_mode})`,
value: '\u200B'
const modeIcon = team.game_mode === 'Squads' ? '🎮' :
team.game_mode === 'Duo' ? '🎯' : '⚔️';
// Combine stats into a single line
const stats = [];
if (team.matches) stats.push(`M:${team.matches}`);
if (team.wins) stats.push(`W:${team.wins}`);
if (team.losses) stats.push(`L:${team.losses}`);
const winRate = team.matches > 0 ?
((parseInt(team.wins) / parseInt(team.matches)) * 100).toFixed(1) : 0;
stats.push(`WR:${winRate}%`);
// Create team header with stats
let teamInfo = `${modeIcon} **${team.name}** (${team.game_name} ${team.game_mode})\n`;
teamInfo += `📊 ${stats.join(' | ')}`;
// Add players if available
if (team.players?.length > 0) {
teamInfo += `\n👥 ${team.players.map(p => p.username).join(', ')}`;
}
// Add logo link if available
if (team.logo) {
teamInfo += `\n🎨 [View Logo](${team.logo})`;
}
infoEmbed.addFields({
name: `Team ${index + 1}`,
value: teamInfo
});
// Add team stats
const statsFields = [];
if (team.matches) statsFields.push(`Matches: ${team.matches}`);
if (team.wins) statsFields.push(`Wins: ${team.wins}`);
if (team.losses) statsFields.push(`Losses: ${team.losses}`);
if (team.forfeits) statsFields.push(`Forfeits: ${team.forfeits}`);
const winRate = team.matches > 0
? ((parseInt(team.wins) / parseInt(team.matches)) * 100).toFixed(1)
: 0;
if (statsFields.length > 0) {
embed.addFields({
name: 'Stats',
value: `${statsFields.join(' | ')} | Win Rate: ${winRate}%`,
inline: false
});
}
// Add team members
if (team.players && team.players.length > 0) {
const playerList = team.players
.map(player => `- ${player.username} (${player.position})`)
.join('\n');
embed.addFields({
name: 'Players',
value: playerList,
inline: false
});
}
// Add team creation date
if (team.date_created) {
embed.addFields({
name: 'Created',
value: new Date(team.date_created).toLocaleDateString(),
inline: true
});
}
// Add separator between teams
// Add small separator if not the last team
if (index < teams.length - 1) {
embed.addFields({ name: '\u200B', value: '\u200B' });
infoEmbed.addFields({ name: '\u200B', value: '▬▬▬▬▬▬▬▬▬▬', inline: false });
}
});
return embed;
embeds.push(infoEmbed);
return embeds;
}
}