Added Chat Page
This commit is contained in:
149
app/api/conversations/[id]/events/route.ts
Normal file
149
app/api/conversations/[id]/events/route.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// Server-Sent Events pour les notifications en temps réel
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return new Response('Non autorisé', { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return new Response('Conversation non trouvée', { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return new Response('Accès non autorisé', { status: 403 });
|
||||
}
|
||||
|
||||
// Créer un stream SSE
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
let isClosed = false;
|
||||
|
||||
// Fonction pour envoyer un événement
|
||||
const sendEvent = (data: any) => {
|
||||
if (isClosed) return;
|
||||
try {
|
||||
const message = `data: ${JSON.stringify(data)}\n\n`;
|
||||
controller.enqueue(encoder.encode(message));
|
||||
} catch (error) {
|
||||
// Le controller est fermé, arrêter le polling
|
||||
isClosed = true;
|
||||
clearInterval(pollInterval);
|
||||
}
|
||||
};
|
||||
|
||||
// Envoyer un événement de connexion
|
||||
sendEvent({ type: 'connected', conversationId: params.id });
|
||||
|
||||
// Polling pour vérifier les nouveaux messages
|
||||
let lastMessageId: string | null = null;
|
||||
const pollInterval = setInterval(async () => {
|
||||
if (isClosed) {
|
||||
clearInterval(pollInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lastMessage = await prisma.message.findFirst({
|
||||
where: {
|
||||
conversationId: params.id,
|
||||
...(lastMessageId ? { id: { gt: lastMessageId } } : {}),
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
files: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (lastMessage) {
|
||||
lastMessageId = lastMessage.id;
|
||||
sendEvent({
|
||||
type: 'new_message',
|
||||
message: lastMessage,
|
||||
});
|
||||
}
|
||||
|
||||
// Vérifier les mises à jour de conversation
|
||||
const updatedConversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedConversation) {
|
||||
sendEvent({
|
||||
type: 'conversation_updated',
|
||||
conversation: updatedConversation,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Si c'est une erreur de controller fermé, arrêter le polling
|
||||
if (error instanceof Error && error.message.includes('closed')) {
|
||||
isClosed = true;
|
||||
clearInterval(pollInterval);
|
||||
} else {
|
||||
console.error('Erreur lors du polling:', error);
|
||||
}
|
||||
}
|
||||
}, 1000); // Poll toutes les secondes
|
||||
|
||||
// Nettoyer lors de la fermeture
|
||||
const cleanup = () => {
|
||||
isClosed = true;
|
||||
clearInterval(pollInterval);
|
||||
try {
|
||||
controller.close();
|
||||
} catch (e) {
|
||||
// Ignorer les erreurs de fermeture
|
||||
}
|
||||
};
|
||||
|
||||
// Gérer la fermeture de la connexion
|
||||
request.signal?.addEventListener('abort', cleanup);
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
});
|
||||
}
|
||||
220
app/api/conversations/[id]/messages/route.ts
Normal file
220
app/api/conversations/[id]/messages/route.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
|
||||
// GET - Récupérer les messages d'une conversation
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const limit = parseInt(searchParams.get('limit') || '50');
|
||||
const cursor = searchParams.get('cursor');
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Mettre à jour lastReadAt pour l'utilisateur
|
||||
await prisma.conversationParticipant.updateMany({
|
||||
where: {
|
||||
conversationId: params.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
lastReadAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const where: any = {
|
||||
conversationId: params.id,
|
||||
};
|
||||
|
||||
if (cursor) {
|
||||
where.id = {
|
||||
lt: cursor,
|
||||
};
|
||||
}
|
||||
|
||||
const messages = await prisma.message.findMany({
|
||||
where,
|
||||
include: {
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
files: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
messages: messages.reverse(), // Inverser pour avoir les plus anciens en premier
|
||||
hasMore: messages.length === limit,
|
||||
cursor: messages.length > 0 ? messages[0].id : null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des messages:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Envoyer un message dans une conversation
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const content = formData.get('content') as string | null;
|
||||
const files = formData.getAll('files') as File[];
|
||||
|
||||
if (!content && (!files || files.length === 0)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le message doit contenir du texte ou au moins un fichier' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Créer le message
|
||||
const message = await prisma.message.create({
|
||||
data: {
|
||||
conversationId: params.id,
|
||||
senderId: user.id,
|
||||
content: content || null,
|
||||
},
|
||||
include: {
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
files: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Traiter les fichiers si présents
|
||||
if (files && files.length > 0) {
|
||||
const uploadDir = join(process.cwd(), 'public', 'uploads', 'messages');
|
||||
|
||||
// Créer le dossier s'il n'existe pas
|
||||
if (!existsSync(uploadDir)) {
|
||||
mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileRecords = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
|
||||
// Générer un nom de fichier unique
|
||||
const timestamp = Date.now();
|
||||
const randomStr = Math.random().toString(36).substring(2, 15);
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
const filename = `${timestamp}-${randomStr}.${fileExtension}`;
|
||||
const filepath = join(uploadDir, filename);
|
||||
|
||||
// Sauvegarder le fichier
|
||||
await writeFile(filepath, buffer);
|
||||
|
||||
// Créer l'enregistrement en base de données
|
||||
return prisma.messageFile.create({
|
||||
data: {
|
||||
messageId: message.id,
|
||||
filename: file.name,
|
||||
filepath: `/uploads/messages/${filename}`,
|
||||
fileType: file.type,
|
||||
fileSize: file.size,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Récupérer le message avec les fichiers
|
||||
const messageWithFiles = await prisma.message.findUnique({
|
||||
where: { id: message.id },
|
||||
include: {
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
files: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Mettre à jour la date de mise à jour de la conversation
|
||||
await prisma.conversation.update({
|
||||
where: { id: params.id },
|
||||
data: { updatedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json(messageWithFiles, { status: 201 });
|
||||
}
|
||||
|
||||
// Mettre à jour la date de mise à jour de la conversation
|
||||
await prisma.conversation.update({
|
||||
where: { id: params.id },
|
||||
data: { updatedAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json(message, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'envoi du message:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
171
app/api/conversations/[id]/participants/route.ts
Normal file
171
app/api/conversations/[id]/participants/route.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// POST - Ajouter des participants à une conversation
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { userIds } = body;
|
||||
|
||||
if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Au moins un utilisateur est requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Seuls les groupes peuvent avoir des participants ajoutés
|
||||
if (conversation.type !== 'group') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Seuls les groupes peuvent avoir des participants ajoutés' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier que les utilisateurs existent
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: userIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (users.length !== userIds.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Un ou plusieurs utilisateurs sont invalides' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Filtrer les utilisateurs déjà participants
|
||||
const existingParticipantIds = conversation.participants.map((p) => p.userId);
|
||||
const newUserIds = userIds.filter((id: string) => !existingParticipantIds.includes(id));
|
||||
|
||||
if (newUserIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tous les utilisateurs sont déjà participants' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Ajouter les nouveaux participants
|
||||
await prisma.conversationParticipant.createMany({
|
||||
data: newUserIds.map((userId: string) => ({
|
||||
conversationId: params.id,
|
||||
userId,
|
||||
})),
|
||||
});
|
||||
|
||||
const updatedConversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedConversation);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'ajout des participants:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Retirer un participant d'une conversation
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const userId = searchParams.get('userId');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'userId est requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Seuls les groupes peuvent avoir des participants retirés
|
||||
if (conversation.type !== 'group') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Seuls les groupes peuvent avoir des participants retirés' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier que le participant existe dans la conversation
|
||||
const participant = conversation.participants.find((p) => p.userId === userId);
|
||||
if (!participant) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Participant non trouvé dans la conversation' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.conversationParticipant.delete({
|
||||
where: { id: participant.id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du retrait du participant:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
94
app/api/conversations/[id]/read/route.ts
Normal file
94
app/api/conversations/[id]/read/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// POST - Marquer les messages comme lus
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Mettre à jour lastReadAt pour l'utilisateur
|
||||
await prisma.conversationParticipant.updateMany({
|
||||
where: {
|
||||
conversationId: params.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
lastReadAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour de la lecture:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// GET - Récupérer les informations de lecture pour une conversation
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: {
|
||||
select: {
|
||||
userId: true,
|
||||
lastReadAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
participants: conversation.participants.map((p) => ({
|
||||
userId: p.userId,
|
||||
lastReadAt: p.lastReadAt,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des lectures:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
163
app/api/conversations/[id]/route.ts
Normal file
163
app/api/conversations/[id]/route.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// GET - Récupérer une conversation spécifique
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(conversation);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération de la conversation:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Mettre à jour une conversation (nom du groupe, etc.)
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { name } = body;
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Seuls les groupes peuvent être renommés
|
||||
if (conversation.type !== 'group') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Seuls les groupes peuvent être renommés' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const updatedConversation = await prisma.conversation.update({
|
||||
where: { id: params.id },
|
||||
data: { name },
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedConversation);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour de la conversation:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Supprimer une conversation (ou quitter un groupe)
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Si c'est un groupe, retirer l'utilisateur
|
||||
// Si c'est une conversation directe, supprimer la conversation
|
||||
if (conversation.type === 'group') {
|
||||
await prisma.conversationParticipant.deleteMany({
|
||||
where: {
|
||||
conversationId: params.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Pour les conversations directes, supprimer complètement
|
||||
await prisma.conversation.delete({
|
||||
where: { id: params.id },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression de la conversation:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
113
app/api/conversations/[id]/typing/route.ts
Normal file
113
app/api/conversations/[id]/typing/route.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// Store temporaire pour les indicateurs de frappe (en production, utiliser Redis ou une DB)
|
||||
const typingUsers = new Map<string, { userId: string; timestamp: number }>();
|
||||
|
||||
// Nettoyer les anciens indicateurs toutes les 5 secondes
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of typingUsers.entries()) {
|
||||
if (now - value.timestamp > 5000) {
|
||||
typingUsers.delete(key);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// POST - Signaler que l'utilisateur est en train d'écrire
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Enregistrer que l'utilisateur est en train d'écrire
|
||||
const key = `${params.id}:${user.id}`;
|
||||
typingUsers.set(key, {
|
||||
userId: user.id,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la signalisation du typing:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// GET - Récupérer les utilisateurs en train d'écrire
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversation = await prisma.conversation.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
|
||||
if (!isParticipant) {
|
||||
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Récupérer les utilisateurs en train d'écrire (sauf l'utilisateur actuel)
|
||||
const typing = Array.from(typingUsers.entries())
|
||||
.filter(([key]) => key.startsWith(`${params.id}:`))
|
||||
.filter(([key]) => !key.endsWith(`:${user.id}`))
|
||||
.map(([, value]) => value.userId);
|
||||
|
||||
// Récupérer les informations des utilisateurs
|
||||
const typingUsersData = await prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: typing,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ typing: typingUsersData });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération du typing:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
236
app/api/conversations/route.ts
Normal file
236
app/api/conversations/route.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// GET - Liste toutes les conversations de l'utilisateur
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const conversations = await prisma.conversation.findMany({
|
||||
where: {
|
||||
participants: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
include: {
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
files: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
// Formater les conversations avec les informations nécessaires
|
||||
const formattedConversations = await Promise.all(
|
||||
conversations.map(async (conv) => {
|
||||
const lastMessage = conv.messages[0] || null;
|
||||
const otherParticipants = conv.participants
|
||||
.filter((p) => p.userId !== user.id)
|
||||
.map((p) => p.user);
|
||||
|
||||
// Pour les conversations directes, utiliser le nom de l'autre participant
|
||||
const displayName =
|
||||
conv.type === 'group'
|
||||
? conv.name || 'Groupe sans nom'
|
||||
: otherParticipants[0]?.name || otherParticipants[0]?.email || 'Utilisateur';
|
||||
|
||||
// Récupérer lastReadAt pour l'utilisateur actuel
|
||||
const userParticipant = conv.participants.find((p) => p.userId === user.id);
|
||||
const lastReadAt = userParticipant?.lastReadAt || null;
|
||||
|
||||
// Compter les messages non lus
|
||||
let unreadCount = 0;
|
||||
if (lastMessage && lastReadAt) {
|
||||
const unreadMessages = await prisma.message.count({
|
||||
where: {
|
||||
conversationId: conv.id,
|
||||
senderId: { not: user.id }, // Exclure les messages de l'utilisateur
|
||||
createdAt: { gt: lastReadAt },
|
||||
},
|
||||
});
|
||||
unreadCount = unreadMessages;
|
||||
} else if (lastMessage && !lastReadAt) {
|
||||
// Si jamais lu, compter tous les messages qui ne sont pas de l'utilisateur
|
||||
const totalUnread = await prisma.message.count({
|
||||
where: {
|
||||
conversationId: conv.id,
|
||||
senderId: { not: user.id },
|
||||
},
|
||||
});
|
||||
unreadCount = totalUnread;
|
||||
}
|
||||
|
||||
return {
|
||||
id: conv.id,
|
||||
name: conv.name,
|
||||
type: conv.type,
|
||||
displayName,
|
||||
participants: conv.participants.map((p) => ({
|
||||
id: p.user.id,
|
||||
email: p.user.email,
|
||||
name: p.user.name,
|
||||
})),
|
||||
lastMessage: lastMessage
|
||||
? {
|
||||
id: lastMessage.id,
|
||||
content: lastMessage.content,
|
||||
senderId: lastMessage.senderId,
|
||||
senderName: lastMessage.sender.name || lastMessage.sender.email,
|
||||
createdAt: lastMessage.createdAt,
|
||||
hasFiles: lastMessage.files.length > 0,
|
||||
}
|
||||
: null,
|
||||
unreadCount,
|
||||
updatedAt: conv.updatedAt,
|
||||
createdAt: conv.createdAt,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json(formattedConversations);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des conversations:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Créer une nouvelle conversation
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { participantIds, name, type } = body;
|
||||
|
||||
if (!participantIds || !Array.isArray(participantIds) || participantIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Au moins un participant est requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Filtrer l'utilisateur actuel de la liste des participants (on ne peut pas créer une conversation avec soi-même)
|
||||
const filteredParticipantIds = participantIds.filter((id: string) => id !== user.id);
|
||||
|
||||
if (filteredParticipantIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Vous ne pouvez pas créer une conversation avec vous-même' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier que tous les participants existent
|
||||
const participants = await prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: filteredParticipantIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (participants.length !== filteredParticipantIds.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Un ou plusieurs participants sont invalides' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Pour les conversations directes, vérifier si une conversation existe déjà
|
||||
if (type === 'direct' && filteredParticipantIds.length === 1) {
|
||||
const existingConversation = await prisma.conversation.findFirst({
|
||||
where: {
|
||||
type: 'direct',
|
||||
participants: {
|
||||
every: {
|
||||
userId: {
|
||||
in: [user.id, filteredParticipantIds[0]],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingConversation) {
|
||||
// Vérifier que les deux participants sont bien dans cette conversation
|
||||
const participantUserIds = existingConversation.participants.map((p) => p.userId);
|
||||
if (
|
||||
participantUserIds.includes(user.id) &&
|
||||
participantUserIds.includes(filteredParticipantIds[0]) &&
|
||||
participantUserIds.length === 2
|
||||
) {
|
||||
return NextResponse.json(existingConversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Créer la conversation avec tous les participants (y compris l'utilisateur actuel)
|
||||
const conversation = await prisma.conversation.create({
|
||||
data: {
|
||||
name: type === 'group' ? name : null,
|
||||
type: type || 'direct',
|
||||
participants: {
|
||||
create: [
|
||||
{ userId: user.id },
|
||||
...filteredParticipantIds.map((id: string) => ({ userId: id })),
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(conversation, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création de la conversation:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user