import { Router, Response } from 'express'; import { z } from 'zod'; import { getDatabase } from '../database'; import { authenticate, AuthRequest } from '../middleware/auth'; const router = Router(); // ─── COLLABORATOR ROUTES ONLY ─── // All project CRUD now goes through WebSocket (see realtime.ts) // Search users by username (for share modal) router.get('/collaborators/search', authenticate, (req: AuthRequest, res: Response) => { const query = req.query.username as string; if (!query || query.length < 2) { res.json({ users: [] }); return; } const db = getDatabase(); const users = db.prepare( 'SELECT id, username FROM users WHERE username LIKE ? AND id != ? LIMIT 20' ).all(`%${query}%`, req.userId); res.json({ users }); }); // List collaborators for a project router.get('/:id/collaborators', authenticate, (req: AuthRequest, res: Response) => { const db = getDatabase(); const project = db.prepare( 'SELECT user_id FROM projects WHERE id = ? AND user_id = ?' ).get(req.params.id, req.userId) as any; let hasAccess = !!project; if (!hasAccess) { const collab = db.prepare( 'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?' ).get(req.params.id, req.userId) as any; hasAccess = !!collab; } if (!hasAccess) { res.status(404).json({ error: 'Project not found' }); return; } const owner = db.prepare( 'SELECT id, username FROM users WHERE id = (SELECT user_id FROM projects WHERE id = ?)' ).get(req.params.id) as any; const collaborators = db.prepare(` SELECT u.id, u.username, c.permission, c.created_at FROM project_collaborators c JOIN users u ON u.id = c.user_id WHERE c.project_id = ? ORDER BY c.created_at ASC `).all(req.params.id); res.json({ owner: owner ? { id: owner.id, username: owner.username } : null, collaborators, }); }); // Add collaborator router.post('/:id/collaborators', authenticate, async (req: AuthRequest, res: Response) => { try { const schema = z.object({ username: z.string().min(1), permission: z.enum(['view', 'edit', 'admin']).default('edit'), }); const { username, permission } = schema.parse(req.body); const db = getDatabase(); const project = db.prepare( 'SELECT user_id FROM projects WHERE id = ?' ).get(req.params.id) as any; if (!project) { res.status(404).json({ error: 'Project not found' }); return; } const isOwner = project.user_id === req.userId; let isAdmin = isOwner; if (!isOwner) { const collab = db.prepare( 'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?' ).get(req.params.id, req.userId) as any; isAdmin = collab?.permission === 'admin'; } if (!isAdmin) { res.status(403).json({ error: 'Only the owner or admin can add collaborators' }); return; } const targetUser = db.prepare( 'SELECT id, username FROM users WHERE username = ?' ).get(username) as any; if (!targetUser) { res.status(404).json({ error: 'User not found' }); return; } if (targetUser.id === project.user_id) { res.status(400).json({ error: 'Cannot add the project owner as a collaborator' }); return; } const existing = db.prepare( 'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?' ).get(req.params.id, targetUser.id) as any; if (existing) { res.status(400).json({ error: 'User is already a collaborator' }); return; } const { v4: uuidv4 } = require('uuid'); db.prepare(` INSERT INTO project_collaborators (project_id, user_id, permission, added_by, created_at) VALUES (?, ?, ?, ?, ?) `).run(req.params.id, targetUser.id, permission, req.userId, Date.now()); res.status(201).json({ collaborator: { id: targetUser.id, username: targetUser.username, permission, }, }); } catch (error: any) { if (error instanceof z.ZodError) { res.status(400).json({ error: 'Invalid input', details: error.errors }); return; } console.error('Add collaborator error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Update collaborator permission router.put('/:id/collaborators/:userId', authenticate, async (req: AuthRequest, res: Response) => { try { const schema = z.object({ permission: z.enum(['view', 'edit', 'admin']), }); const { permission } = schema.parse(req.body); const db = getDatabase(); const project = db.prepare( 'SELECT user_id FROM projects WHERE id = ?' ).get(req.params.id) as any; if (!project) { res.status(404).json({ error: 'Project not found' }); return; } const isOwner = project.user_id === req.userId; let isAdmin = isOwner; if (!isOwner) { const collab = db.prepare( 'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?' ).get(req.params.id, req.userId) as any; isAdmin = collab?.permission === 'admin'; } if (!isAdmin) { res.status(403).json({ error: 'Permission denied' }); return; } const result = db.prepare(` UPDATE project_collaborators SET permission = ? WHERE project_id = ? AND user_id = ? `).run(permission, req.params.id, req.params.userId); if (result.changes === 0) { res.status(404).json({ error: 'Collaborator not found' }); return; } res.json({ message: 'Permission updated', permission }); } catch (error: any) { if (error instanceof z.ZodError) { res.status(400).json({ error: 'Invalid input', details: error.errors }); return; } console.error('Update collaborator error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Remove collaborator router.delete('/:id/collaborators/:userId', authenticate, (req: AuthRequest, res: Response) => { const db = getDatabase(); const project = db.prepare( 'SELECT user_id FROM projects WHERE id = ?' ).get(req.params.id) as any; if (!project) { res.status(404).json({ error: 'Project not found' }); return; } const isOwner = project.user_id === req.userId; const isSelf = req.userId === req.params.userId; let isAdmin = isOwner; if (!isOwner && !isSelf) { const collab = db.prepare( 'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?' ).get(req.params.id, req.userId) as any; isAdmin = collab?.permission === 'admin'; } if (!isOwner && !isAdmin && !isSelf) { res.status(403).json({ error: 'Permission denied' }); return; } const result = db.prepare( 'DELETE FROM project_collaborators WHERE project_id = ? AND user_id = ?' ).run(req.params.id, req.userId); if (result.changes === 0) { res.status(404).json({ error: 'Collaborator not found' }); return; } res.json({ message: 'Collaborator removed' }); }); export default router;