Spaces:
Sleeping
Sleeping
| 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; | |