feat(chat): add 2000 character limit for messages
Added message length validation to prevent spam and improve UX with character counter feedback. Backend: - Added MESSAGE_MAX_LENGTH constant (2000 characters) - Validation in send_event_message handler: - Check if content is string - Check if not empty after trim - Check if not exceeding max length - Validation in send_match_message handler (same checks) - Returns error message if validation fails Frontend: - Added MESSAGE_MAX_LENGTH constant (2000 characters) - ChatInput component enhancements: - maxLength attribute on input (hard limit) - Character counter shows when >80% of limit - Counter turns red when at limit - Submit button disabled when at limit - Counter format: "X/2000" UX: - User sees counter at 1600+ characters (80% of limit) - Hard limit prevents typing beyond 2000 chars - Clear visual feedback (red text) when at limit - Consistent validation on both event and match chats Security: - Prevents spam with extremely long messages - Protects against potential DoS via message size - Database already uses TEXT type (supports limit)
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
const { MATCH_STATUS, SUGGESTION_STATUS } = require('./statuses');
|
const { MATCH_STATUS, SUGGESTION_STATUS } = require('./statuses');
|
||||||
const { ACCOUNT_TIER, FAIRNESS_SUPPORTER_PENALTY, FAIRNESS_COMFORT_PENALTY } = require('./tiers');
|
const { ACCOUNT_TIER, FAIRNESS_SUPPORTER_PENALTY, FAIRNESS_COMFORT_PENALTY } = require('./tiers');
|
||||||
|
|
||||||
|
// Message validation
|
||||||
|
const MESSAGE_MAX_LENGTH = 2000;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
MATCH_STATUS,
|
MATCH_STATUS,
|
||||||
SUGGESTION_STATUS,
|
SUGGESTION_STATUS,
|
||||||
ACCOUNT_TIER,
|
ACCOUNT_TIER,
|
||||||
FAIRNESS_SUPPORTER_PENALTY,
|
FAIRNESS_SUPPORTER_PENALTY,
|
||||||
FAIRNESS_COMFORT_PENALTY,
|
FAIRNESS_COMFORT_PENALTY,
|
||||||
|
MESSAGE_MAX_LENGTH,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { Server } = require('socket.io');
|
|||||||
const { verifyToken } = require('../utils/auth');
|
const { verifyToken } = require('../utils/auth');
|
||||||
const { prisma } = require('../utils/db');
|
const { prisma } = require('../utils/db');
|
||||||
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
||||||
|
const { MESSAGE_MAX_LENGTH } = require('../constants');
|
||||||
|
|
||||||
// Track active users in each event room
|
// Track active users in each event room
|
||||||
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
||||||
@@ -310,6 +311,19 @@ function initializeSocket(httpServer) {
|
|||||||
return socket.emit('error', { message: 'Not in an event room' });
|
return socket.emit('error', { message: 'Not in an event room' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate message content
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return socket.emit('error', { message: 'Invalid message content' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.trim().length === 0) {
|
||||||
|
return socket.emit('error', { message: 'Message cannot be empty' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length > MESSAGE_MAX_LENGTH) {
|
||||||
|
return socket.emit('error', { message: `Message too long. Maximum ${MESSAGE_MAX_LENGTH} characters allowed.` });
|
||||||
|
}
|
||||||
|
|
||||||
const eventId = socket.currentEventId;
|
const eventId = socket.currentEventId;
|
||||||
const roomName = socket.currentEventRoom;
|
const roomName = socket.currentEventRoom;
|
||||||
|
|
||||||
@@ -434,6 +448,19 @@ function initializeSocket(httpServer) {
|
|||||||
// Send message to match room
|
// Send message to match room
|
||||||
socket.on('send_match_message', async ({ matchId, content }) => {
|
socket.on('send_match_message', async ({ matchId, content }) => {
|
||||||
try {
|
try {
|
||||||
|
// Validate message content
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return socket.emit('error', { message: 'Invalid message content' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.trim().length === 0) {
|
||||||
|
return socket.emit('error', { message: 'Message cannot be empty' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length > MESSAGE_MAX_LENGTH) {
|
||||||
|
return socket.emit('error', { message: `Message too long. Maximum ${MESSAGE_MAX_LENGTH} characters allowed.` });
|
||||||
|
}
|
||||||
|
|
||||||
const roomName = `match_${matchId}`;
|
const roomName = `match_${matchId}`;
|
||||||
|
|
||||||
// Get match and its room
|
// Get match and its room
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Send } from 'lucide-react';
|
import { Send } from 'lucide-react';
|
||||||
|
import { MESSAGE_MAX_LENGTH } from '../../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chat Input component with send button
|
* Chat Input component with send button and character counter
|
||||||
*
|
*
|
||||||
* @param {string} value - Input value
|
* @param {string} value - Input value
|
||||||
* @param {function} onChange - Change handler for input
|
* @param {function} onChange - Change handler for input
|
||||||
@@ -26,7 +27,12 @@ const ChatInput = ({
|
|||||||
onSubmit(e);
|
onSubmit(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const charCount = value.length;
|
||||||
|
const isNearLimit = charCount > MESSAGE_MAX_LENGTH * 0.8; // 80% of limit
|
||||||
|
const isOverLimit = charCount > MESSAGE_MAX_LENGTH;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
<form onSubmit={handleSubmit} className={`flex space-x-2 ${className}`}>
|
<form onSubmit={handleSubmit} className={`flex space-x-2 ${className}`}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -35,17 +41,28 @@ const ChatInput = ({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
|
maxLength={MESSAGE_MAX_LENGTH}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={disabled || !value.trim()}
|
disabled={disabled || !value.trim() || isOverLimit}
|
||||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
aria-label="Send message"
|
aria-label="Send message"
|
||||||
>
|
>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{/* Character counter - show when approaching limit */}
|
||||||
|
{isNearLimit && (
|
||||||
|
<div className="text-right">
|
||||||
|
<span className={`text-xs ${isOverLimit ? 'text-red-600' : 'text-gray-500'}`}>
|
||||||
|
{charCount}/{MESSAGE_MAX_LENGTH}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,6 @@ export {
|
|||||||
CONNECTION_STATE,
|
CONNECTION_STATE,
|
||||||
SUGGESTION_TYPE,
|
SUGGESTION_TYPE,
|
||||||
} from './statuses';
|
} from './statuses';
|
||||||
|
|
||||||
|
// Message validation
|
||||||
|
export const MESSAGE_MAX_LENGTH = 2000;
|
||||||
|
|||||||
Reference in New Issue
Block a user