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:
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "matches" ADD COLUMN "slug" VARCHAR(50) NOT NULL DEFAULT gen_random_uuid()::text;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "matches_slug_key" ON "matches"("slug");
|
||||
@@ -131,6 +131,7 @@ model Message {
|
||||
// Matches (pairs of users for collaboration)
|
||||
model Match {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @default(cuid()) @db.VarChar(50)
|
||||
user1Id Int @map("user1_id")
|
||||
user2Id Int @map("user2_id")
|
||||
eventId Int @map("event_id")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user