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
413 lines (361 loc) • 11.8 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 createChannelSchema = z.object({
name: z.string().min(1, 'Channel name is required').max(100, 'Name too long'),
description: z.string().max(1000, 'Description too long').optional(),
handle: z.string().min(3, 'Handle too short').max(30, 'Handle too long').regex(/^[a-zA-Z0-9_-]+$/, 'Handle can only contain letters, numbers, hyphens, and underscores'),
avatar: z.string().url().optional(),
banner: z.string().url().optional(),
category: z.enum(['GENERAL', 'GAMING', 'MUSIC', 'SPORTS', 'ENTERTAINMENT', 'NEWS', 'EDUCATION', 'SCIENCE_TECH', 'TRAVEL', 'LIFESTYLE', 'COOKING', 'FITNESS', 'BUSINESS', 'COMEDY', 'DOCUMENTARY', 'KIDS', 'LIVE']).default('GENERAL'),
socialLinks: z.record(z.string().url()).optional()
});
const updateChannelSchema = createChannelSchema.partial();
const subscribeSchema = z.object({
channelId: z.string(),
enableNotifications: z.boolean().default(true)
});
async function hasChannelPermission(userId: string, channelId: string, action: string = 'read') {
const channel = await prisma.videoChannel.findUnique({
where: { id: channelId },
include: {
collaborators: {
where: { userId, isActive: true }
}
}
});
if (!channel) return false;
// Channel owner has all permissions
if (channel.ownerId === userId) return true;
// Check collaborator permissions
const collaborator = channel.collaborators[0];
if (!collaborator) return false;
switch (action) {
case 'read':
return true;
case 'update':
return ['ADMIN', 'MODERATOR', 'EDITOR'].includes(collaborator.role);
case 'delete':
return ['ADMIN'].includes(collaborator.role);
case 'upload':
return ['ADMIN', 'MODERATOR', 'EDITOR'].includes(collaborator.role);
case 'moderate':
return ['ADMIN', 'MODERATOR'].includes(collaborator.role);
default:
return false;
}
}
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 } = req;
switch (method) {
case 'GET':
return await getChannels(req, res);
case 'POST':
const { action } = req.query;
if (action === 'subscribe') {
return await subscribeToChannel(req, res);
}
return await createChannel(req, res);
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).end('Method Not Allowed');
}
} catch (error) {
console.error('Channels API error:', error);
return res.status(500).json({ error: 'Internal server error' });
} finally {
await prisma.$disconnect();
}
}
async function getChannels(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const {
category,
search,
sortBy = 'subscriberCount',
sortOrder = 'desc',
limit = '20',
page = '1',
featured = 'false',
verified = 'false'
} = req.query;
try {
const pageNum = parseInt(page as string);
const limitNum = parseInt(limit as string);
const offset = (pageNum - 1) * limitNum;
// Build where clause
const whereClause: any = {};
if (category && category !== 'ALL') {
whereClause.category = category;
}
if (verified === 'true') {
whereClause.isVerified = true;
}
if (search && typeof search === 'string') {
whereClause.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
{ handle: { contains: search, mode: 'insensitive' } }
];
}
// Get channels with comprehensive data
const channels = await prisma.videoChannel.findMany({
where: whereClause,
include: {
owner: {
select: {
id: true,
name: true,
email: true,
image: true
}
},
_count: {
select: {
videos: true,
subscribers: true,
playlists: true
}
}
},
orderBy: {
[sortBy as string]: sortOrder
},
skip: offset,
take: limitNum
});
// Enrich channels with user subscription status
const enrichedChannels = await Promise.all(
channels.map(async (channel) => {
// Check if current user is subscribed
const subscription = await prisma.channelSubscription.findUnique({
where: {
channelId_userId: {
channelId: channel.id,
userId: session.user.id
}
}
});
// Get recent video count (last 30 days)
const recentVideoCount = await prisma.video.count({
where: {
channelId: channel.id,
publishedAt: {
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
},
status: 'READY',
visibility: 'PUBLIC'
}
});
return {
...channel,
isSubscribed: !!subscription,
recentVideoCount,
stats: {
totalSubscribers: channel._count.subscribers,
totalVideos: channel._count.videos,
totalPlaylists: channel._count.playlists,
recentVideos: recentVideoCount
}
};
})
);
// Get total count for pagination
const total = await prisma.videoChannel.count({
where: whereClause
});
res.status(200).json({
channels: enrichedChannels,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum)
}
});
} catch (error) {
console.error('Error fetching channels:', error);
res.status(500).json({ error: 'Failed to fetch channels' });
}
}
async function createChannel(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
try {
const validatedData = createChannelSchema.parse(req.body);
// Check if handle is unique
const existingChannel = await prisma.videoChannel.findUnique({
where: { handle: validatedData.handle }
});
if (existingChannel) {
return res.status(400).json({ error: 'Handle already exists' });
}
// Check user channel limits (max 3 channels per user for non-verified users)
const userChannelCount = await prisma.videoChannel.count({
where: { ownerId: session.user.id }
});
if (userChannelCount >= 3) {
return res.status(400).json({
error: 'Maximum channel limit reached (3 channels)'
});
}
// Create the channel
const channel = await prisma.videoChannel.create({
data: {
...validatedData,
ownerId: session.user.id,
socialLinks: validatedData.socialLinks ? JSON.stringify(validatedData.socialLinks) : undefined
},
include: {
owner: {
select: {
id: true,
name: true,
email: true,
image: true
}
}
}
});
// Auto-subscribe owner to their own channel
await prisma.channelSubscription.create({
data: {
channelId: channel.id,
userId: session.user.id,
isNotificationsEnabled: true
}
});
// Create initial analytics entry
await prisma.channelAnalytics.create({
data: {
channelId: channel.id,
date: new Date(),
subscriberCount: 1, // Owner subscription
totalViews: 0,
totalWatchTime: 0,
videoPublished: 0
}
});
res.status(201).json({
channel,
message: 'Channel created successfully'
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error creating channel:', error);
res.status(500).json({ error: 'Failed to create channel' });
}
}
async function subscribeToChannel(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
try {
const validatedData = subscribeSchema.parse(req.body);
// Check if channel exists
const channel = await prisma.videoChannel.findUnique({
where: { id: validatedData.channelId }
});
if (!channel) {
return res.status(404).json({ error: 'Channel not found' });
}
// Check if user is trying to subscribe to their own channel
if (channel.ownerId === session.user.id) {
return res.status(400).json({ error: 'Cannot subscribe to your own channel' });
}
// Check current subscription status
const existingSubscription = await prisma.channelSubscription.findUnique({
where: {
channelId_userId: {
channelId: validatedData.channelId,
userId: session.user.id
}
}
});
if (existingSubscription) {
// Update notification settings if already subscribed
const updatedSubscription = await prisma.channelSubscription.update({
where: { id: existingSubscription.id },
data: {
isNotificationsEnabled: validatedData.enableNotifications
}
});
return res.status(200).json({
subscription: updatedSubscription,
message: 'Subscription settings updated'
});
}
// Create new subscription
const subscription = await prisma.channelSubscription.create({
data: {
channelId: validatedData.channelId,
userId: session.user.id,
isNotificationsEnabled: validatedData.enableNotifications
},
include: {
channel: {
select: {
id: true,
name: true,
handle: true,
avatar: true
}
}
}
});
// Update channel subscriber count
await prisma.videoChannel.update({
where: { id: validatedData.channelId },
data: {
subscriberCount: {
increment: 1
}
}
});
// Update today's analytics
const today = new Date();
today.setHours(0, 0, 0, 0);
await prisma.channelAnalytics.upsert({
where: {
channelId_date: {
channelId: validatedData.channelId,
date: today
}
},
update: {
subscriberCount: channel.subscriberCount + 1
},
create: {
channelId: validatedData.channelId,
date: today,
subscriberCount: channel.subscriberCount + 1,
totalViews: 0,
totalWatchTime: 0,
videoPublished: 0
}
});
res.status(201).json({
subscription,
message: 'Successfully subscribed to channel'
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
console.error('Error subscribing to channel:', error);
res.status(500).json({ error: 'Failed to subscribe to channel' });
}
}