2025-07-25 09:34:28 -04:00
|
|
|
import { sql } from '@vercel/postgres';
|
|
|
|
|
import { Resend } from 'resend';
|
|
|
|
|
|
|
|
|
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
|
|
|
|
|
|
|
|
export default async function handler(req, res) {
|
|
|
|
|
// Set CORS headers
|
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
|
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
|
|
|
|
|
|
|
|
// Handle preflight requests
|
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
|
|
|
res.status(200).end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { id } = req.query; // collection id
|
|
|
|
|
|
|
|
|
|
if (req.method === 'GET') {
|
|
|
|
|
// Get all permissions for a collection
|
|
|
|
|
try {
|
|
|
|
|
const result = await sql`
|
|
|
|
|
SELECT
|
|
|
|
|
cp.*,
|
|
|
|
|
u.email,
|
|
|
|
|
u.id as user_id,
|
|
|
|
|
u.is_pending
|
|
|
|
|
FROM collection_permissions cp
|
|
|
|
|
JOIN users u ON cp.user_id = u.id
|
|
|
|
|
WHERE cp.collection_id = ${id}
|
|
|
|
|
ORDER BY cp.role, cp.created_at
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
res.status(200).json(result.rows);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching permissions:', error);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
} else if (req.method === 'POST') {
|
|
|
|
|
// Invite user to collection
|
|
|
|
|
try {
|
|
|
|
|
const { email, role = 'viewer', message = '' } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!email || !['owner', 'editor', 'viewer'].includes(role)) {
|
|
|
|
|
return res.status(400).json({ error: 'Valid email and role are required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if user exists
|
|
|
|
|
const userResult = await sql`
|
|
|
|
|
SELECT id, email FROM users WHERE email = ${email}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
let userId;
|
|
|
|
|
if (userResult.rows.length === 0) {
|
|
|
|
|
// Create pending user record
|
|
|
|
|
const newUserResult = await sql`
|
|
|
|
|
INSERT INTO users (email, password, role, is_pending)
|
|
|
|
|
VALUES (${email}, '', 'user', true)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`;
|
|
|
|
|
userId = newUserResult.rows[0].id;
|
|
|
|
|
} else {
|
|
|
|
|
userId = userResult.rows[0].id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if permission already exists
|
|
|
|
|
const existingPermission = await sql`
|
|
|
|
|
SELECT * FROM collection_permissions
|
|
|
|
|
WHERE collection_id = ${id} AND user_id = ${userId}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
if (existingPermission.rows.length > 0) {
|
|
|
|
|
return res.status(409).json({ error: 'User already has access to this collection' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get collection details for email
|
|
|
|
|
const collectionResult = await sql`
|
|
|
|
|
SELECT c.name, u.email as owner_email
|
|
|
|
|
FROM collections c
|
|
|
|
|
JOIN users u ON c.user_id = u.id
|
|
|
|
|
WHERE c.id = ${id}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
if (collectionResult.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ error: 'Collection not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const collection = collectionResult.rows[0];
|
|
|
|
|
|
|
|
|
|
// Create permission record
|
|
|
|
|
const permissionResult = await sql`
|
|
|
|
|
INSERT INTO collection_permissions (collection_id, user_id, role, status, invited_by)
|
|
|
|
|
VALUES (${id}, ${userId}, ${role}, 'pending', 1)
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Generate invitation token
|
|
|
|
|
const inviteToken = Buffer.from(`${id}:${userId}:${Date.now()}`).toString('base64');
|
|
|
|
|
|
|
|
|
|
await sql`
|
|
|
|
|
UPDATE collection_permissions
|
|
|
|
|
SET invite_token = ${inviteToken}
|
|
|
|
|
WHERE id = ${permissionResult.rows[0].id}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Send invitation email
|
|
|
|
|
const acceptUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/invite/accept?token=${inviteToken}`;
|
|
|
|
|
const declineUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/invite/decline?token=${inviteToken}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await resend.emails.send({
|
|
|
|
|
from: 'TCG Vault <noreply@tcgvault.com>',
|
|
|
|
|
to: email,
|
|
|
|
|
subject: `You've been invited to collaborate on "${collection.name}"`,
|
|
|
|
|
html: `
|
|
|
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
|
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; color: white;">
|
|
|
|
|
<h1 style="margin: 0; font-size: 28px;">🃏 TCG Vault</h1>
|
|
|
|
|
<p style="margin: 10px 0 0 0; opacity: 0.9;">Collection Collaboration Invite</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="padding: 30px; background: #f8f9fa;">
|
|
|
|
|
<h2 style="color: #333; margin-top: 0;">You've been invited to collaborate!</h2>
|
|
|
|
|
|
|
|
|
|
<p style="color: #666; line-height: 1.6;">
|
|
|
|
|
<strong>${collection.owner_email}</strong> has invited you to collaborate on the collection
|
|
|
|
|
<strong>"${collection.name}"</strong> with <strong>${role}</strong> permissions.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
${message ? `
|
|
|
|
|
<div style="background: #e3f2fd; padding: 15px; border-left: 4px solid #2196f3; margin: 20px 0;">
|
|
|
|
|
<p style="margin: 0; color: #1976d2;"><strong>Personal message:</strong></p>
|
|
|
|
|
<p style="margin: 5px 0 0 0; color: #333;">"${message}"</p>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<div style="margin: 30px 0;">
|
2025-07-25 11:29:45 -04:00
|
|
|
<h3 style="color: #333;">What you can do as a ${role === 'editor' ? 'collaborator' : role}:</h3>
|
2025-07-25 09:34:28 -04:00
|
|
|
<ul style="color: #666; line-height: 1.8;">
|
2025-07-25 11:29:45 -04:00
|
|
|
${role === 'editor' ? `
|
|
|
|
|
<li>Add and remove cards from the collection</li>
|
|
|
|
|
<li>Edit collection details and description</li>
|
|
|
|
|
<li>View and search all collection content</li>
|
|
|
|
|
<li>Help build and organize the collection</li>
|
2025-07-25 09:34:28 -04:00
|
|
|
` : `
|
|
|
|
|
<li>View all collection content</li>
|
|
|
|
|
<li>Browse and search cards</li>
|
2025-07-25 11:29:45 -04:00
|
|
|
<li>See collection statistics and details</li>
|
2025-07-25 09:34:28 -04:00
|
|
|
`}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="text-align: center; margin: 30px 0;">
|
|
|
|
|
<a href="${acceptUrl}" style="background: #4caf50; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; font-weight: bold; margin-right: 10px; display: inline-block;">
|
|
|
|
|
Accept Invitation
|
|
|
|
|
</a>
|
|
|
|
|
<a href="${declineUrl}" style="background: #f44336; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block;">
|
|
|
|
|
Decline
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="border-top: 1px solid #ddd; padding-top: 20px; margin-top: 30px; color: #999; font-size: 14px;">
|
|
|
|
|
<p>This invitation will expire in 7 days. If you have any questions, please contact ${collection.owner_email}.</p>
|
|
|
|
|
<p>If you didn't expect this invitation, you can safely ignore this email.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
|
|
|
|
});
|
|
|
|
|
} catch (emailError) {
|
|
|
|
|
console.error('Email sending failed:', emailError);
|
|
|
|
|
// Continue anyway - the invitation is still created
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log activity
|
|
|
|
|
await sql`
|
|
|
|
|
INSERT INTO collection_activity (collection_id, user_id, action, details)
|
|
|
|
|
VALUES (${id}, 1, 'user_invited', ${JSON.stringify({ email, role, inviteToken })})
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
message: 'Invitation sent successfully',
|
|
|
|
|
permission: {
|
|
|
|
|
...permissionResult.rows[0],
|
|
|
|
|
email,
|
|
|
|
|
invite_token: inviteToken
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error inviting user:', error);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
} else if (req.method === 'PUT') {
|
|
|
|
|
// Update user permission
|
|
|
|
|
try {
|
|
|
|
|
const { userId, role, status } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!userId || !['owner', 'editor', 'viewer'].includes(role)) {
|
|
|
|
|
return res.status(400).json({ error: 'Valid user ID and role are required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await sql`
|
|
|
|
|
UPDATE collection_permissions
|
|
|
|
|
SET role = ${role}, status = ${status || 'active'}, updated_at = NOW()
|
|
|
|
|
WHERE collection_id = ${id} AND user_id = ${userId}
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ error: 'Permission not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log activity
|
|
|
|
|
await sql`
|
|
|
|
|
INSERT INTO collection_activity (collection_id, user_id, action, details)
|
|
|
|
|
VALUES (${id}, 1, 'permission_updated', ${JSON.stringify({ userId, role, status })})
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
res.status(200).json(result.rows[0]);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error updating permission:', error);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
} else if (req.method === 'DELETE') {
|
|
|
|
|
// Remove user permission
|
|
|
|
|
try {
|
|
|
|
|
const { userId } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
return res.status(400).json({ error: 'User ID is required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await sql`
|
|
|
|
|
DELETE FROM collection_permissions
|
|
|
|
|
WHERE collection_id = ${id} AND user_id = ${userId}
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ error: 'Permission not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log activity
|
|
|
|
|
await sql`
|
|
|
|
|
INSERT INTO collection_activity (collection_id, user_id, action, details)
|
|
|
|
|
VALUES (${id}, 1, 'user_removed', ${JSON.stringify({ userId })})
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
res.status(200).json({ message: 'Permission removed successfully' });
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error removing permission:', error);
|
|
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
res.status(405).json({ error: 'Method not allowed' });
|
|
|
|
|
}
|
|
|
|
|
}
|