// pages/api/send.ts import { prisma } from '@/lib/prisma'; import { UPLOAD_DIR } from '@/lib/config'; import { PLAN_CONFIG } from '@/lib/subscription'; import { sendMailjetEmail } from '@/lib/mailjet'; import { v4 as uuidv4 } from 'uuid'; import bcrypt from 'bcrypt'; import path from 'path'; import { IncomingForm, File as FormidableFile } from 'formidable'; import fs from 'fs'; import { promises as fsPromises } from 'fs'; import { NextApiRequest, NextApiResponse } from 'next'; import { getServerSession } from 'next-auth/next'; import { authOptions } from '@/lib/auth'; export const config = { api: { bodyParser: false, }, }; interface ParsedFields { [key: string]: string | string[] | undefined; } interface ParsedFiles { [key: string]: FormidableFile | FormidableFile[] | undefined; } export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ success: false, message: 'Method not allowed' }); } const session = await getServerSession(req, res, authOptions); if (!session?.user?.email) { return res.status(401).json({ success: false, message: 'Unauthorized' }); } const form = new IncomingForm({ uploadDir: UPLOAD_DIR, keepExtensions: true, maxFileSize: 100 * 1024 * 1024 * 1024, // 100GB }); const parseForm = (): Promise<{ fields: ParsedFields; files: ParsedFiles }> => new Promise((resolve, reject) => { form.parse(req, (err: Error | null, fields: any, files: any) => { if (err) return reject(err); resolve({ fields, files }); }); }); try { const { fields, files } = await parseForm(); // Extract form fields const encryptedFile = Array.isArray(files.file) ? files.file[0] : files.file; const recipient = Array.isArray(fields.email) ? fields.email[0] : fields.email; const password = Array.isArray(fields.password) ? fields.password[0] : fields.password; // Sender is taken from the session, never the request body, so a caller // cannot post transfers (and consume quota) as another user. const sender = session.user.email; const filenames = Array.isArray(fields.filenames) ? fields.filenames[0] : fields.filenames; const message = Array.isArray(fields.message) ? fields.message[0] : fields.message; const encryptionIvStr = Array.isArray(fields.encryptionIv) ? fields.encryptionIv[0] : fields.encryptionIv; const originalFilename = Array.isArray(fields.originalFilename) ? fields.originalFilename[0] : fields.originalFilename; if (!encryptedFile || !recipient || !sender) { return res.status(400).json({ success: false, message: 'Missing required fields' }); } // Ensure upload directory exists if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); } // Generate final filename and move encrypted file const finalFilename = uuidv4() + '.enc'; const finalPath = path.join(UPLOAD_DIR, finalFilename); // Use streaming copy for large files to avoid permission issues await new Promise((resolve, reject) => { const readStream = fs.createReadStream(encryptedFile.filepath); const writeStream = fs.createWriteStream(finalPath); readStream.on('error', reject); writeStream.on('error', reject); writeStream.on('finish', resolve); readStream.pipe(writeStream); }); // Clean up temp file after successful copy try { await fsPromises.unlink(encryptedFile.filepath); } catch (err) { console.warn('Failed to clean up temp file:', err); } // Verify user exists const user = await prisma.user.findUnique({ where: { email: sender } }); if (!user) { fs.unlinkSync(finalPath); return res.status(404).json({ success: false, message: 'User not found' }); } const plan = (user.plan || 'free') as 'free' | 'rookie' | 'pro'; const limits = PLAN_CONFIG[plan]; // Check file size against plan limits const stats = fs.statSync(finalPath); if (limits.maxFileSize !== Infinity && stats.size > limits.maxFileSize) { fs.unlinkSync(finalPath); return res.status(413).json({ success: false, message: `File exceeds max size for your plan (${plan})` }); } // Check monthly transfer limits const startOfMonth = new Date(); startOfMonth.setDate(1); startOfMonth.setHours(0, 0, 0, 0); const transfersThisMonth = await prisma.transfer.findMany({ where: { senderEmail: sender, createdAt: { gte: startOfMonth }, }, }); const totalSizeSentThisMonth = transfersThisMonth.reduce((acc, t) => acc + BigInt(t.totalSize), BigInt(0)); if ( limits.maxTransfersPerMonth !== Infinity && transfersThisMonth.length >= limits.maxTransfersPerMonth || limits.maxTransferSizePerMonth !== Infinity && totalSizeSentThisMonth + BigInt(stats.size) > BigInt(limits.maxTransferSizePerMonth) ) { fs.unlinkSync(finalPath); return res.status(429).json({ success: false, message: 'Plan limits exceeded' }); } // Hash password if provided const hash = password ? await bcrypt.hash(password, 10) : null; // Parse filenames if provided let totalFiles = 1; if (filenames) { try { const filesArray = JSON.parse(filenames); if (Array.isArray(filesArray)) { totalFiles = filesArray.length; } } catch {} } const expiresAt = new Date(Date.now() + limits.maxExpiryMs); // Create transfer record with encrypted file const transfer = await prisma.transfer.create({ data: { senderEmail: sender, recipientEmail: recipient, passwordHash: hash, downloadUrl: uuidv4(), expiresAt, filenames: filenames || null, message: message || null, totalFiles, totalSize: stats.size, userId: user?.id, files: { create: [ { name: originalFilename || 'file', path: finalPath, size: stats.size, }, ], }, }, }); // Update file with encryption IV using raw query if (encryptionIvStr) { await (prisma as any).$executeRaw` UPDATE "File" SET "encryptionIv" = ${encryptionIvStr} WHERE "transferId" = ${transfer.id} `; } // Send email notification const link = `${process.env.NEXT_PUBLIC_APP_URL}/download/${transfer.downloadUrl}`; await sendMailjetEmail({ to: recipient, subject: 'You\'ve received an encrypted file', text: ` ${sender} sent you an encrypted file via TransferTribe. Link: ${link} ${message ? `Message:\n${message}\n\n` : ''}Note: You'll need the password they shared with you to decrypt it. `.trim(), }); return res.status(200).json({ success: true }); } catch (err: any) { console.error('Upload failed:', err); return res.status(500).json({ success: false, message: 'Upload failed: ' + err.message }); } }