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:
Radosław Gierwiało
2025-11-13 21:43:58 +01:00
parent 20f405cab3
commit b2c2527c46
8 changed files with 127 additions and 37 deletions

View File

@@ -13,6 +13,7 @@ router.get('/', authenticate, async (req, res, next) => {
const events = await prisma.event.findMany({
select: {
id: true,
slug: true,
name: true,
location: true,
startDate: true,
@@ -35,6 +36,7 @@ router.get('/', authenticate, async (req, res, next) => {
// Transform data and add isJoined flag
const eventsWithJoinedStatus = events.map(event => ({
id: event.id,
slug: event.slug,
name: event.name,
location: event.location,
startDate: event.startDate,
@@ -67,14 +69,14 @@ router.get('/', authenticate, async (req, res, next) => {
}
});
// GET /api/events/:id - Get event by ID
router.get('/:id', async (req, res, next) => {
// GET /api/events/:slug - Get event by slug
router.get('/:slug', async (req, res, next) => {
try {
const { id } = req.params;
const { slug } = req.params;
const event = await prisma.event.findUnique({
where: {
id: parseInt(id),
slug: slug,
},
include: {
chatRooms: true,
@@ -102,16 +104,29 @@ router.get('/:id', async (req, res, next) => {
}
});
// GET /api/events/:eventId/messages - Get event chat messages with pagination
router.get('/:eventId/messages', authenticate, async (req, res, next) => {
// GET /api/events/:slug/messages - Get event chat messages with pagination
router.get('/:slug/messages', authenticate, async (req, res, next) => {
try {
const { eventId } = req.params;
const { slug } = req.params;
const { before, limit = 20 } = req.query;
// Find event by slug
const event = await prisma.event.findUnique({
where: { slug },
select: { id: true },
});
if (!event) {
return res.status(404).json({
success: false,
error: 'Event not found',
});
}
// Find event chat room
const chatRoom = await prisma.chatRoom.findFirst({
where: {
eventId: parseInt(eventId),
eventId: event.id,
type: 'event',
},
});