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
text/typescript
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' });
}
}