🌊 TeamFlow — Modern Trello alternative with email integration
- Full-stack: React 18 + Express + SQLite - Drag-and-drop kanban boards with @hello-pangea/dnd - Google App Password email integration (SMTP + IMAP) - Inbound email: create cards by sending emails - Reply-to-card: email replies become comments - Admin/user management with role-based access - Setup wizard: email config → admin creation - Checklists, time tracking, priorities, labels, due dates - Real-time notifications with activity feed - Beautiful HTML email templates
This commit is contained in:
129
server/routes/boards.js
Normal file
129
server/routes/boards.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db.js';
|
||||
import { authMiddleware } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
router.use(authMiddleware);
|
||||
|
||||
const BACKGROUNDS = [
|
||||
'gradient-blue', 'gradient-purple', 'gradient-green', 'gradient-orange',
|
||||
'gradient-pink', 'gradient-teal', 'gradient-indigo', 'gradient-red',
|
||||
];
|
||||
|
||||
function canAccessBoard(userId, boardId) {
|
||||
const m = db.prepare('SELECT 1 FROM board_members WHERE board_id = ? AND user_id = ?').get(boardId, userId);
|
||||
return !!m;
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const boards = db.prepare(`
|
||||
SELECT b.*, bm.role as my_role,
|
||||
(SELECT COUNT(*) FROM lists l JOIN cards c ON c.list_id = l.id WHERE l.board_id = b.id AND c.assigned_to = ? AND c.due_date IS NOT NULL AND date(c.due_date) <= date('now', '+3 days')) as due_soon_count
|
||||
FROM boards b
|
||||
JOIN board_members bm ON bm.board_id = b.id AND bm.user_id = ?
|
||||
WHERE b.is_archived = 0
|
||||
ORDER BY b.created_at DESC
|
||||
`).all(req.user.id, req.user.id);
|
||||
res.json(boards);
|
||||
});
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
const { title, description, background } = req.body;
|
||||
if (!title) return res.status(400).json({ error: 'Title required' });
|
||||
const bg = background && BACKGROUNDS.includes(background) ? background : BACKGROUNDS[Math.floor(Math.random() * BACKGROUNDS.length)];
|
||||
const result = db.prepare('INSERT INTO boards (title, description, background, created_by) VALUES (?, ?, ?, ?)')
|
||||
.run(title, description || '', bg, req.user.id);
|
||||
db.prepare('INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, ?)').run(result.lastInsertRowid, req.user.id, 'admin');
|
||||
const defaultLists = ['To Do', 'In Progress', 'Review', 'Done'];
|
||||
const insertList = db.prepare('INSERT INTO lists (board_id, title, position) VALUES (?, ?, ?)');
|
||||
defaultLists.forEach((l, i) => insertList.run(result.lastInsertRowid, l, i * 65536));
|
||||
const board = db.prepare('SELECT * FROM boards WHERE id = ?').get(result.lastInsertRowid);
|
||||
board.my_role = 'admin';
|
||||
res.json(board);
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
if (!canAccessBoard(req.user.id, req.params.id)) return res.status(403).json({ error: 'Access denied' });
|
||||
const board = db.prepare('SELECT * FROM boards WHERE id = ?').get(req.params.id);
|
||||
res.json(board);
|
||||
});
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
if (!canAccessBoard(req.user.id, req.params.id)) return res.status(403).json({ error: 'Access denied' });
|
||||
const { title, description, background, is_archived } = req.body;
|
||||
const board = db.prepare('SELECT * FROM boards WHERE id = ?').get(req.params.id);
|
||||
if (!board) return res.status(404).json({ error: 'Not found' });
|
||||
if (title !== undefined) db.prepare('UPDATE boards SET title = ? WHERE id = ?').run(title, req.params.id);
|
||||
if (description !== undefined) db.prepare('UPDATE boards SET description = ? WHERE id = ?').run(description, req.params.id);
|
||||
if (background !== undefined) db.prepare('UPDATE boards SET background = ? WHERE id = ?').run(background, req.params.id);
|
||||
if (is_archived !== undefined) db.prepare('UPDATE boards SET is_archived = ? WHERE id = ?').run(is_archived ? 1 : 0, req.params.id);
|
||||
const updated = db.prepare('SELECT * FROM boards WHERE id = ?').get(req.params.id);
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
if (!canAccessBoard(req.user.id, req.params.id)) return res.status(403).json({ error: 'Access denied' });
|
||||
db.prepare('DELETE FROM boards WHERE id = ?').run(req.params.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.get('/:id/full', (req, res) => {
|
||||
if (!canAccessBoard(req.user.id, req.params.id)) return res.status(403).json({ error: 'Access denied' });
|
||||
const board = db.prepare('SELECT * FROM boards WHERE id = ?').get(req.params.id);
|
||||
const lists = db.prepare('SELECT * FROM lists WHERE board_id = ? AND is_archived = 0 ORDER BY position').all(req.params.id);
|
||||
const labels = db.prepare('SELECT * FROM labels WHERE board_id = ? ORDER BY name').all(req.params.id);
|
||||
const members = db.prepare(`
|
||||
SELECT u.id, u.email, u.name, u.avatar_color, bm.role as board_role
|
||||
FROM board_members bm JOIN users u ON u.id = bm.user_id WHERE bm.board_id = ?
|
||||
`).all(req.params.id);
|
||||
|
||||
const cards = db.prepare(`
|
||||
SELECT c.*,
|
||||
u1.name as creator_name, u1.avatar_color as creator_color,
|
||||
u2.name as assignee_name, u2.avatar_color as assignee_color,
|
||||
(SELECT COUNT(*) FROM checklist_items ci JOIN checklists cl ON cl.id = ci.checklist_id WHERE cl.card_id = c.id) as total_items,
|
||||
(SELECT COUNT(*) FROM checklist_items ci JOIN checklists cl ON cl.id = ci.checklist_id WHERE cl.card_id = c.id AND ci.is_checked = 1) as done_items
|
||||
FROM cards c
|
||||
LEFT JOIN users u1 ON u1.id = c.created_by
|
||||
LEFT JOIN users u2 ON u2.id = c.assigned_to
|
||||
WHERE c.list_id IN (SELECT id FROM lists WHERE board_id = ? AND is_archived = 0)
|
||||
ORDER BY c.position
|
||||
`).all(req.params.id);
|
||||
|
||||
const cardLabels = db.prepare(`
|
||||
SELECT cl.card_id, l.id as label_id, l.name, l.color
|
||||
FROM card_labels cl JOIN labels l ON l.id = cl.label_id
|
||||
WHERE l.board_id = ?
|
||||
`).all(req.params.id);
|
||||
|
||||
const cardsById = {};
|
||||
cards.forEach(c => { c.labels = []; cardsById[c.id] = c; });
|
||||
cardLabels.forEach(cl => { if (cardsById[cl.card_id]) cardsById[cl.card_id].labels.push({ id: cl.label_id, name: cl.name, color: cl.color }); });
|
||||
|
||||
const listsWithCards = lists.map(l => ({
|
||||
...l,
|
||||
cards: cards.filter(c => c.list_id === l.id),
|
||||
}));
|
||||
|
||||
res.json({ ...board, lists: listsWithCards, labels, members });
|
||||
});
|
||||
|
||||
router.post('/:id/members', (req, res) => {
|
||||
if (!canAccessBoard(req.user.id, req.params.id)) return res.status(403).json({ error: 'Access denied' });
|
||||
const { user_id, role } = req.body;
|
||||
if (!user_id) return res.status(400).json({ error: 'User ID required' });
|
||||
const existing = db.prepare('SELECT 1 FROM board_members WHERE board_id = ? AND user_id = ?').get(req.params.id, user_id);
|
||||
if (existing) return res.status(400).json({ error: 'Already a member' });
|
||||
db.prepare('INSERT INTO board_members (board_id, user_id, role) VALUES (?, ?, ?)').run(req.params.id, user_id, role || 'member');
|
||||
const member = db.prepare(`SELECT u.id, u.email, u.name, u.avatar_color, bm.role as board_role
|
||||
FROM board_members bm JOIN users u ON u.id = bm.user_id WHERE bm.board_id = ? AND bm.user_id = ?`).get(req.params.id, user_id);
|
||||
res.json(member);
|
||||
});
|
||||
|
||||
router.delete('/:id/members/:userId', (req, res) => {
|
||||
if (!canAccessBoard(req.user.id, req.params.id)) return res.status(403).json({ error: 'Access denied' });
|
||||
db.prepare('DELETE FROM board_members WHERE board_id = ? AND user_id = ?').run(req.params.id, req.params.userId);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user