fix: prevent bypassing event check-in via page refresh
Users could gain unauthorized access to event chats by refreshing the page after leaving an event. The socket handler was automatically creating participation records when users joined rooms, completely bypassing the QR code check-in requirement. This fix verifies that users have legitimately checked in before allowing socket room access.
This commit is contained in:
@@ -69,26 +69,27 @@ function initializeSocket(httpServer) {
|
||||
|
||||
const eventId = event.id;
|
||||
const roomName = `event_${eventId}`;
|
||||
socket.join(roomName);
|
||||
socket.currentEventRoom = roomName;
|
||||
socket.currentEventId = eventId;
|
||||
socket.currentEventSlug = slug;
|
||||
|
||||
// Record event participation in database
|
||||
await prisma.eventParticipant.upsert({
|
||||
// Verify that user has checked in to this event
|
||||
const participant = await prisma.eventParticipant.findUnique({
|
||||
where: {
|
||||
userId_eventId: {
|
||||
userId: socket.user.id,
|
||||
eventId: eventId,
|
||||
},
|
||||
},
|
||||
update: {}, // Don't update anything if already exists
|
||||
create: {
|
||||
userId: socket.user.id,
|
||||
eventId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!participant) {
|
||||
socket.emit('error', { message: 'You must check in to this event first' });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.join(roomName);
|
||||
socket.currentEventRoom = roomName;
|
||||
socket.currentEventId = eventId;
|
||||
socket.currentEventSlug = slug;
|
||||
|
||||
// Add user to active users
|
||||
if (!activeUsers.has(eventId)) {
|
||||
activeUsers.set(eventId, new Set());
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Send, UserPlus, Loader2, LogOut, AlertTriangle } from 'lucide-react';
|
||||
import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode } from 'lucide-react';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||
import { eventsAPI } from '../services/api';
|
||||
|
||||
@@ -11,6 +11,7 @@ const EventChatPage = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [event, setEvent] = useState(null);
|
||||
const [isParticipant, setIsParticipant] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
@@ -27,16 +28,27 @@ const EventChatPage = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
// Fetch event data
|
||||
// Fetch event data and check participation
|
||||
useEffect(() => {
|
||||
const fetchEvent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await eventsAPI.getBySlug(slug);
|
||||
setEvent(data);
|
||||
|
||||
// Get all events with participation info
|
||||
const allEvents = await eventsAPI.getAll();
|
||||
const eventData = allEvents.find(e => e.slug === slug);
|
||||
|
||||
if (eventData) {
|
||||
setEvent(eventData);
|
||||
setIsParticipant(eventData.isJoined);
|
||||
} else {
|
||||
setEvent(null);
|
||||
setIsParticipant(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading event:', err);
|
||||
setEvent(null);
|
||||
setIsParticipant(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -237,6 +249,55 @@ const EventChatPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is participant
|
||||
if (!isParticipant) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-md p-8">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<QrCode className="w-8 h-8 text-amber-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Check-in Required
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
You need to check in at the event venue by scanning the QR code to access this chat.
|
||||
</p>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm text-amber-800 font-medium mb-1">
|
||||
{event.name}
|
||||
</p>
|
||||
<p className="text-sm text-amber-700">
|
||||
{event.location}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Link
|
||||
to="/events"
|
||||
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Back to Events
|
||||
</Link>
|
||||
{import.meta.env.DEV && (
|
||||
<Link
|
||||
to={`/events/${slug}/details`}
|
||||
className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
View QR Code (dev)
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
|
||||
Reference in New Issue
Block a user