UNPKG

sysrot-hub

Version:

CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026

884 lines (773 loc) 23.6 kB
import { NextApiRequest, NextApiResponse } from 'next'; import { PrismaClient } from '@prisma/client'; import { getSession } from 'next-auth/react'; import { z } from 'zod'; const prisma = new PrismaClient(); // Validation schemas const createRoomSchema = z.object({ name: z.string().min(1, 'Room name is required').max(100, 'Room name too long'), description: z.string().max(500, 'Description too long').optional(), type: z.enum(['DIRECT', 'GROUP', 'CHANNEL', 'SUPPORT']).default('GROUP'), isPrivate: z.boolean().default(false), avatar: z.string().url().optional(), maxMembers: z.number().min(2).max(1000).optional(), participants: z.array(z.string()).optional(), // User IDs to invite settings: z.record(z.any()).optional() }); const updateRoomSchema = createRoomSchema.partial().omit({ participants: true }); const addParticipantSchema = z.object({ userId: z.string().min(1, 'User ID is required'), role: z.enum(['OWNER', 'ADMIN', 'MODERATOR', 'MEMBER', 'GUEST']).default('MEMBER'), permissions: z.array(z.string()).optional() }); async function hasRoomPermission(userId: string, roomId: string, requiredRoles: string[] = ['OWNER', 'ADMIN']) { const participant = await prisma.chatParticipant.findUnique({ where: { roomId_userId: { roomId, userId } }, include: { room: true } }); if (!participant) return false; if (participant.isBlocked) return false; return requiredRoles.includes(participant.role) || participant.room.createdBy === userId; } async function getUserParticipation(userId: string, roomId: string) { return await prisma.chatParticipant.findUnique({ where: { roomId_userId: { roomId, userId } }, include: { room: true, user: { select: { id: true, name: true, email: true, image: true } } } }); } export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const session = await getSession({ req }); if (!session?.user) { return res.status(401).json({ error: 'Authentication required' }); } const { method, query } = req; const { id, action } = query; if (action === 'join' && method === 'POST') { return await joinRoom(req, res); } else if (action === 'leave' && method === 'POST') { return await leaveRoom(req, res); } else if (action === 'participants' && method === 'GET') { return await getRoomParticipants(req, res); } else if (action === 'participants' && method === 'POST') { return await addParticipant(req, res); } else if (action === 'participants' && method === 'DELETE') { return await removeParticipant(req, res); } switch (method) { case 'GET': if (id) { return await getRoom(req, res); } else { return await getUserRooms(req, res); } case 'POST': return await createRoom(req, res); case 'PUT': return await updateRoom(req, res); case 'DELETE': return await deleteRoom(req, res); default: res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']); return res.status(405).end('Method Not Allowed'); } } catch (error) { console.error('Chat rooms API error:', error); return res.status(500).json({ error: 'Internal server error' }); } finally { await prisma.$disconnect(); } } async function getUserRooms(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { type, limit = '20' } = req.query; try { const whereClause: any = { participants: { some: { userId: session.user.id, isBlocked: false } } }; if (type && typeof type === 'string') { whereClause.type = type.toUpperCase(); } const rooms = await prisma.chatRoom.findMany({ where: whereClause, include: { participants: { include: { user: { select: { id: true, name: true, email: true, image: true } } }, orderBy: { joinedAt: 'asc' } }, messages: { take: 1, orderBy: { createdAt: 'desc' }, include: { user: { select: { id: true, name: true, image: true } }, readReceipts: { where: { userId: session.user.id } } } }, _count: { select: { messages: true, participants: true } } }, orderBy: [ { messages: { _count: 'desc' } }, { updatedAt: 'desc' } ], take: parseInt(limit as string) }); // Enrich rooms with user-specific data const enrichedRooms = rooms.map(room => { const userParticipation = room.participants.find(p => p.userId === session.user.id); const lastMessage = room.messages[0]; const hasUnread = lastMessage && userParticipation?.lastSeenAt && lastMessage.createdAt > userParticipation.lastSeenAt; return { ...room, userRole: userParticipation?.role, lastSeenAt: userParticipation?.lastSeenAt, isMuted: userParticipation?.isMuted, hasUnread, unreadCount: hasUnread ? 1 : 0, // Simplified, could be more accurate lastMessage: lastMessage || null }; }); res.status(200).json({ rooms: enrichedRooms }); } catch (error) { console.error('Error fetching user rooms:', error); res.status(500).json({ error: 'Failed to fetch rooms' }); } } async function getRoom(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { // Check if user has access to this room const participation = await getUserParticipation(session.user.id, id); if (!participation || participation.isBlocked) { return res.status(403).json({ error: 'Access denied' }); } const room = await prisma.chatRoom.findUnique({ where: { id }, include: { creator: { select: { id: true, name: true, email: true, image: true } }, participants: { include: { user: { select: { id: true, name: true, email: true, image: true } } }, orderBy: [ { role: 'asc' }, { joinedAt: 'asc' } ] }, _count: { select: { messages: true, participants: true } } } }); if (!room) { return res.status(404).json({ error: 'Room not found' }); } // Update user's last seen timestamp await prisma.chatParticipant.update({ where: { roomId_userId: { roomId: id, userId: session.user.id } }, data: { lastSeenAt: new Date() } }); res.status(200).json({ room: { ...room, userRole: participation.role, userPermissions: participation.permissions } }); } catch (error) { console.error('Error fetching room:', error); res.status(500).json({ error: 'Failed to fetch room' }); } } async function createRoom(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); try { const validatedData = createRoomSchema.parse(req.body); const { participants, ...roomData } = validatedData; // For direct messages, ensure only 2 participants if (roomData.type === 'DIRECT') { if (!participants || participants.length !== 1) { return res.status(400).json({ error: 'Direct messages require exactly one other participant' }); } // Check if direct room already exists const existingDirectRoom = await prisma.chatRoom.findFirst({ where: { type: 'DIRECT', participants: { every: { userId: { in: [session.user.id, participants[0]] } } } }, include: { participants: true } }); if (existingDirectRoom && existingDirectRoom.participants.length === 2) { return res.status(200).json({ room: existingDirectRoom, message: 'Direct room already exists' }); } } // Create the room const room = await prisma.chatRoom.create({ data: { ...roomData, createdBy: session.user.id }, include: { creator: { select: { id: true, name: true, email: true, image: true } } } }); // Add creator as owner await prisma.chatParticipant.create({ data: { roomId: room.id, userId: session.user.id, role: 'OWNER', permissions: ['*'] // Full permissions } }); // Add other participants if (participants && participants.length > 0) { const participantData = participants.map(userId => ({ roomId: room.id, userId, role: roomData.type === 'DIRECT' ? 'MEMBER' : 'MEMBER', permissions: [] })); await prisma.chatParticipant.createMany({ data: participantData, skipDuplicates: true }); // Send system message await prisma.chatMessage.create({ data: { roomId: room.id, content: `${session.user.name} created the ${roomData.type.toLowerCase()} "${room.name}"`, type: 'SYSTEM' } }); } res.status(201).json({ room, message: 'Room created successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error creating room:', error); res.status(500).json({ error: 'Failed to create room' }); } } async function updateRoom(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { // Check permissions const hasAccess = await hasRoomPermission(session.user.id, id, ['OWNER', 'ADMIN']); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } const validatedData = updateRoomSchema.parse(req.body); const room = await prisma.chatRoom.update({ where: { id }, data: validatedData, include: { creator: { select: { id: true, name: true, email: true, image: true } }, _count: { select: { participants: true, messages: true } } } }); // Log system message for significant changes if (validatedData.name || validatedData.description) { await prisma.chatMessage.create({ data: { roomId: id, content: `${session.user.name} updated the room settings`, type: 'SYSTEM' } }); } res.status(200).json({ room, message: 'Room updated successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error updating room:', error); res.status(500).json({ error: 'Failed to update room' }); } } async function deleteRoom(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { // Check if user is owner or has permission const hasAccess = await hasRoomPermission(session.user.id, id, ['OWNER']); if (!hasAccess) { return res.status(403).json({ error: 'Only room owners can delete rooms' }); } // Get room info for logging const room = await prisma.chatRoom.findUnique({ where: { id }, select: { name: true, type: true } }); if (!room) { return res.status(404).json({ error: 'Room not found' }); } // Delete room (cascade will handle related records) await prisma.chatRoom.delete({ where: { id } }); res.status(200).json({ message: 'Room deleted successfully' }); } catch (error) { console.error('Error deleting room:', error); res.status(500).json({ error: 'Failed to delete room' }); } } async function joinRoom(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { const room = await prisma.chatRoom.findUnique({ where: { id }, include: { participants: true, _count: { select: { participants: true } } } }); if (!room) { return res.status(404).json({ error: 'Room not found' }); } // Check if room is private if (room.isPrivate) { return res.status(403).json({ error: 'Cannot join private room without invitation' }); } // Check member limit if (room.maxMembers && room._count.participants >= room.maxMembers) { return res.status(400).json({ error: 'Room has reached maximum capacity' }); } // Check if already a participant const existingParticipant = room.participants.find(p => p.userId === session.user.id); if (existingParticipant) { if (existingParticipant.isBlocked) { return res.status(403).json({ error: 'You are blocked from this room' }); } return res.status(200).json({ message: 'Already a member of this room' }); } // Add user as participant const participant = await prisma.chatParticipant.create({ data: { roomId: id, userId: session.user.id, role: 'MEMBER', permissions: [] }, include: { user: { select: { id: true, name: true, email: true, image: true } } } }); // Send system message await prisma.chatMessage.create({ data: { roomId: id, content: `${session.user.name} joined the ${room.type.toLowerCase()}`, type: 'SYSTEM' } }); res.status(200).json({ participant, message: 'Successfully joined room' }); } catch (error) { console.error('Error joining room:', error); res.status(500).json({ error: 'Failed to join room' }); } } async function leaveRoom(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { const participant = await prisma.chatParticipant.findUnique({ where: { roomId_userId: { roomId: id, userId: session.user.id } }, include: { room: true } }); if (!participant) { return res.status(404).json({ error: 'Not a member of this room' }); } // Owners cannot leave their own rooms - they must transfer ownership or delete if (participant.role === 'OWNER') { return res.status(400).json({ error: 'Owners cannot leave their rooms. Transfer ownership or delete the room.' }); } // Remove participant await prisma.chatParticipant.delete({ where: { roomId_userId: { roomId: id, userId: session.user.id } } }); // Send system message await prisma.chatMessage.create({ data: { roomId: id, content: `${session.user.name} left the ${participant.room.type.toLowerCase()}`, type: 'SYSTEM' } }); res.status(200).json({ message: 'Successfully left room' }); } catch (error) { console.error('Error leaving room:', error); res.status(500).json({ error: 'Failed to leave room' }); } } async function getRoomParticipants(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { // Check if user has access to this room const userParticipation = await getUserParticipation(session.user.id, id); if (!userParticipation || userParticipation.isBlocked) { return res.status(403).json({ error: 'Access denied' }); } const participants = await prisma.chatParticipant.findMany({ where: { roomId: id, isBlocked: false }, include: { user: { select: { id: true, name: true, email: true, image: true } } }, orderBy: [ { role: 'asc' }, { joinedAt: 'asc' } ] }); res.status(200).json({ participants }); } catch (error) { console.error('Error fetching participants:', error); res.status(500).json({ error: 'Failed to fetch participants' }); } } async function addParticipant(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Room ID is required' }); } try { const validatedData = addParticipantSchema.parse(req.body); const { userId, role, permissions } = validatedData; // Check permissions const hasAccess = await hasRoomPermission(session.user.id, id, ['OWNER', 'ADMIN', 'MODERATOR']); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } // Check if user exists const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, name: true, email: true, image: true } }); if (!user) { return res.status(404).json({ error: 'User not found' }); } // Check if already a participant const existingParticipant = await prisma.chatParticipant.findUnique({ where: { roomId_userId: { roomId: id, userId } } }); if (existingParticipant) { return res.status(400).json({ error: 'User is already a participant' }); } // Check room capacity const room = await prisma.chatRoom.findUnique({ where: { id }, include: { _count: { select: { participants: true } } } }); if (!room) { return res.status(404).json({ error: 'Room not found' }); } if (room.maxMembers && room._count.participants >= room.maxMembers) { return res.status(400).json({ error: 'Room has reached maximum capacity' }); } // Add participant const participant = await prisma.chatParticipant.create({ data: { roomId: id, userId, role, permissions: permissions || [] }, include: { user: { select: { id: true, name: true, email: true, image: true } } } }); // Send system message await prisma.chatMessage.create({ data: { roomId: id, content: `${session.user.name} added ${user.name} to the ${room.type.toLowerCase()}`, type: 'SYSTEM' } }); res.status(201).json({ participant, message: 'Participant added successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error adding participant:', error); res.status(500).json({ error: 'Failed to add participant' }); } } async function removeParticipant(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); const { id, userId } = req.query; if (!id || typeof id !== 'string' || !userId || typeof userId !== 'string') { return res.status(400).json({ error: 'Room ID and User ID are required' }); } try { // Check permissions const hasAccess = await hasRoomPermission(session.user.id, id, ['OWNER', 'ADMIN', 'MODERATOR']); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } const participant = await prisma.chatParticipant.findUnique({ where: { roomId_userId: { roomId: id, userId } }, include: { user: { select: { name: true } }, room: { select: { type: true } } } }); if (!participant) { return res.status(404).json({ error: 'Participant not found' }); } // Cannot remove room owner if (participant.role === 'OWNER') { return res.status(400).json({ error: 'Cannot remove room owner' }); } // Remove participant await prisma.chatParticipant.delete({ where: { roomId_userId: { roomId: id, userId } } }); // Send system message await prisma.chatMessage.create({ data: { roomId: id, content: `${session.user.name} removed ${participant.user.name} from the ${participant.room.type.toLowerCase()}`, type: 'SYSTEM' } }); res.status(200).json({ message: 'Participant removed successfully' }); } catch (error) { console.error('Error removing participant:', error); res.status(500).json({ error: 'Failed to remove participant' }); } }