feat: add event slugs to prevent ID enumeration attacks
Replace sequential event IDs in URLs with unique alphanumeric slugs to prevent enumeration attacks. Event URLs now use format /events/{slug}/chat instead of /events/{id}/chat.
Backend changes:
- Add slug field (VARCHAR 50, unique) to Event model
- Create migration with auto-generated 12-char MD5-based slugs for existing events
- Update GET /api/events/:slug endpoint (changed from :id)
- Update GET /api/events/:slug/messages endpoint (changed from :eventId)
- Modify Socket.IO join_event_room to accept slug parameter
- Update send_event_message to use stored event context instead of passing eventId
Frontend changes:
- Update eventsAPI.getBySlug() method (changed from getById)
- Update eventsAPI.getMessages() to use slug parameter
- Change route from /events/:eventId/chat to /events/:slug/chat
- Update EventsPage to navigate using event.slug
- Update EventChatPage to fetch event data via slug and use slug in socket events
Security impact: Prevents attackers from discovering all events by iterating sequential IDs.
This commit is contained in:
@@ -2,15 +2,16 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { mockEvents } from '../mocks/events';
|
||||
import { Send, UserPlus, Loader2 } from 'lucide-react';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||
import { eventsAPI } from '../services/api';
|
||||
|
||||
const EventChatPage = () => {
|
||||
const { eventId } = useParams();
|
||||
const { slug } = useParams();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [event, setEvent] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [activeUsers, setActiveUsers] = useState([]);
|
||||
@@ -20,17 +21,35 @@ const EventChatPage = () => {
|
||||
const messagesEndRef = useRef(null);
|
||||
const messagesContainerRef = useRef(null);
|
||||
|
||||
const event = mockEvents.find(e => e.id === parseInt(eventId));
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
// Fetch event data
|
||||
useEffect(() => {
|
||||
const fetchEvent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await eventsAPI.getBySlug(slug);
|
||||
setEvent(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading event:', err);
|
||||
setEvent(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEvent();
|
||||
}, [slug]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!event) return;
|
||||
|
||||
// Connect to Socket.IO
|
||||
const socket = connectSocket();
|
||||
|
||||
@@ -43,7 +62,7 @@ const EventChatPage = () => {
|
||||
socket.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
// Join event room
|
||||
socket.emit('join_event_room', { eventId: parseInt(eventId) });
|
||||
socket.emit('join_event_room', { slug });
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
@@ -84,7 +103,7 @@ const EventChatPage = () => {
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
socket.emit('leave_event_room', { eventId: parseInt(eventId) });
|
||||
socket.emit('leave_event_room');
|
||||
socket.off('connect');
|
||||
socket.off('disconnect');
|
||||
socket.off('message_history');
|
||||
@@ -93,7 +112,7 @@ const EventChatPage = () => {
|
||||
socket.off('user_joined');
|
||||
socket.off('user_left');
|
||||
};
|
||||
}, [eventId, user.id]);
|
||||
}, [event, slug, user.id]);
|
||||
|
||||
const handleSendMessage = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -107,7 +126,6 @@ const EventChatPage = () => {
|
||||
|
||||
// Send message via Socket.IO
|
||||
socket.emit('send_event_message', {
|
||||
eventId: parseInt(eventId),
|
||||
content: newMessage,
|
||||
});
|
||||
|
||||
@@ -120,7 +138,7 @@ const EventChatPage = () => {
|
||||
setLoadingOlder(true);
|
||||
try {
|
||||
const oldestMessageId = messages[0].id;
|
||||
const response = await eventsAPI.getMessages(eventId, oldestMessageId, 20);
|
||||
const response = await eventsAPI.getMessages(slug, oldestMessageId, 20);
|
||||
|
||||
if (response.data.length > 0) {
|
||||
// Save current scroll position
|
||||
@@ -170,10 +188,27 @@ const EventChatPage = () => {
|
||||
return () => container.removeEventListener('scroll', handleScroll);
|
||||
}, [loadingOlder, hasMore, messages]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-4xl mx-auto flex items-center justify-center min-h-[400px]">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="w-12 h-12 animate-spin text-primary-600" />
|
||||
<p className="text-gray-600">Loading event...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="text-center">Event not found</div>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4 text-red-700">
|
||||
Event not found
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ const EventsPage = () => {
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
const handleJoinEvent = (eventId) => {
|
||||
navigate(`/events/${eventId}/chat`);
|
||||
const handleJoinEvent = (slug) => {
|
||||
navigate(`/events/${slug}/chat`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -102,7 +102,7 @@ const EventsPage = () => {
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleJoinEvent(event.id)}
|
||||
onClick={() => handleJoinEvent(event.slug)}
|
||||
className="w-full px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
{event.isJoined ? 'Open chat' : 'Join chat'}
|
||||
|
||||
Reference in New Issue
Block a user