deckhearth/pages/api/collections/[id]/permissions.js

263 lines
9.6 KiB
JavaScript
Raw Normal View History

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;">
<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;">
${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>
<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' });
}
}