RealBlocks / server /src /routes /projects.ts
incognitolm
Phase A
06a5f5e
Raw
History Blame Contribute Delete
7.09 kB
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;