feat: add match slugs for security and fix message history loading

Security improvements:
- Add random CUID slugs to Match model to prevent ID enumeration attacks
- Update all match URLs from /matches/:id to /matches/:slug
- Keep numeric IDs for internal Socket.IO operations only

Backend changes:
- Add slug field to matches table with unique index
- Update all match endpoints to use slug-based lookups (GET, PUT, DELETE)
- Add GET /api/matches/:slug/messages endpoint to fetch message history
- Include matchSlug in all Socket.IO notifications

Frontend changes:
- Update all match routes to use slug parameter
- Update MatchesPage to use slug for accept/reject/navigate operations
- Update MatchChatPage to fetch match data by slug and load message history
- Update RatePartnerPage to use slug parameter
- Add matchesAPI.getMatchMessages() function

Bug fixes:
- Fix MatchChatPage not loading message history from database on mount
- Messages now persist and display correctly when users reconnect
This commit is contained in:
Radosław Gierwiało
2025-11-14 22:22:11 +01:00
parent 4a3e32f3b6
commit c2010246e3
8 changed files with 201 additions and 53 deletions

View File

@@ -135,6 +135,7 @@ router.post('/', authenticate, async (req, res, next) => {
const targetSocketRoom = `user_${targetUserId}`;
io.to(targetSocketRoom).emit('match_request_received', {
matchId: match.id,
matchSlug: match.slug,
from: {
id: match.user1.id,
username: match.user1.username,
@@ -255,6 +256,7 @@ router.get('/', authenticate, async (req, res, next) => {
return {
id: match.id,
slug: match.slug,
partner: {
id: partner.id,
username: partner.username,
@@ -280,14 +282,94 @@ router.get('/', authenticate, async (req, res, next) => {
}
});
// GET /api/matches/:id - Get specific match
router.get('/:id', authenticate, async (req, res, next) => {
// GET /api/matches/:slug/messages - Get messages for a match
router.get('/:slug/messages', authenticate, async (req, res, next) => {
try {
const { id } = req.params;
const { slug } = req.params;
const userId = req.user.id;
// Find match
const match = await prisma.match.findUnique({
where: { slug },
select: {
id: true,
user1Id: true,
user2Id: true,
roomId: true,
status: true,
},
});
if (!match) {
return res.status(404).json({
success: false,
error: 'Match not found',
});
}
// Check authorization
if (match.user1Id !== userId && match.user2Id !== userId) {
return res.status(403).json({
success: false,
error: 'You are not authorized to view messages for this match',
});
}
// Check if match is accepted
if (match.status !== 'accepted' || !match.roomId) {
return res.status(400).json({
success: false,
error: 'Match must be accepted before viewing messages',
});
}
// Get messages
const messages = await prisma.message.findMany({
where: { roomId: match.roomId },
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
firstName: true,
lastName: true,
},
},
},
orderBy: { createdAt: 'asc' },
take: 100, // Last 100 messages
});
res.json({
success: true,
count: messages.length,
data: messages.map(msg => ({
id: msg.id,
roomId: msg.roomId,
userId: msg.user.id,
username: msg.user.username,
avatar: msg.user.avatar,
firstName: msg.user.firstName,
lastName: msg.user.lastName,
content: msg.content,
type: msg.type,
createdAt: msg.createdAt,
})),
});
} catch (error) {
next(error);
}
});
// GET /api/matches/:slug - Get specific match
router.get('/:slug', authenticate, async (req, res, next) => {
try {
const { slug } = req.params;
const userId = req.user.id;
const match = await prisma.match.findUnique({
where: { id: parseInt(id) },
where: { slug },
include: {
user1: {
select: {
@@ -349,6 +431,7 @@ router.get('/:id', authenticate, async (req, res, next) => {
success: true,
data: {
id: match.id,
slug: match.slug,
partner: {
id: partner.id,
username: partner.username,
@@ -368,15 +451,15 @@ router.get('/:id', authenticate, async (req, res, next) => {
}
});
// PUT /api/matches/:id/accept - Accept a pending match
router.put('/:id/accept', authenticate, async (req, res, next) => {
// PUT /api/matches/:slug/accept - Accept a pending match
router.put('/:slug/accept', authenticate, async (req, res, next) => {
try {
const { id } = req.params;
const { slug } = req.params;
const userId = req.user.id;
// Find match
const match = await prisma.match.findUnique({
where: { id: parseInt(id) },
where: { slug },
include: {
user1: {
select: {
@@ -437,7 +520,7 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
// Update match status and link to chat room
const updated = await tx.match.update({
where: { id: parseInt(id) },
where: { slug },
data: {
status: 'accepted',
roomId: chatRoom.id,
@@ -487,6 +570,7 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
const notification = {
matchId: updatedMatch.id,
matchSlug: updatedMatch.slug,
roomId: updatedMatch.roomId,
event: {
slug: updatedMatch.event.slug,
@@ -523,6 +607,7 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
success: true,
data: {
id: updatedMatch.id,
slug: updatedMatch.slug,
partner: {
id: partner.id,
username: partner.username,
@@ -542,15 +627,15 @@ router.put('/:id/accept', authenticate, async (req, res, next) => {
}
});
// DELETE /api/matches/:id - Reject or cancel a match
router.delete('/:id', authenticate, async (req, res, next) => {
// DELETE /api/matches/:slug - Reject or cancel a match
router.delete('/:slug', authenticate, async (req, res, next) => {
try {
const { id } = req.params;
const { slug } = req.params;
const userId = req.user.id;
// Find match
const match = await prisma.match.findUnique({
where: { id: parseInt(id) },
where: { slug },
include: {
event: {
select: {
@@ -586,7 +671,7 @@ router.delete('/:id', authenticate, async (req, res, next) => {
// Delete match (will cascade delete chat room if exists)
await prisma.match.delete({
where: { id: parseInt(id) },
where: { slug },
});
// Emit socket event to the other user
@@ -597,6 +682,7 @@ router.delete('/:id', authenticate, async (req, res, next) => {
io.to(otherUserSocketRoom).emit('match_cancelled', {
matchId: match.id,
matchSlug: match.slug,
event: {
slug: match.event.slug,
name: match.event.name,