import { prisma } from '../prisma.js'; import { generateSecureKey, hashKey, verifyKeyHash } from '../utils/crypto.js'; export class BoardService { /** * Create a new Kanban board with admin and member keys and standard agile columns with WIP limits. */ static async createBoard(title = 'Agile Kanban Board') { const adminKey = generateSecureKey(32); const memberKey = generateSecureKey(32); const adminKeyHash = hashKey(adminKey); const memberKeyHash = hashKey(memberKey); const board = await prisma.board.create({ data: { title: title.trim() || 'Agile Kanban Board', adminKeyHash, memberKeyHash, memberKey, autoMoveStale: false, columns: { create: [ { title: '📋 Backlog', order: 0, maxWipLimit: 0, autoStaleHours: 0 }, { title: '📝 To Do', order: 1, maxWipLimit: 0, autoStaleHours: 0 }, { title: '⚡ In Progress', order: 2, maxWipLimit: 3, autoStaleHours: 24 }, // Standard 3-card WIP limit & 24h stale { title: '🔍 Review', order: 3, maxWipLimit: 4, autoStaleHours: 24 }, { title: '✅ Done', order: 4, maxWipLimit: 0, autoStaleHours: 0 }, ], }, }, }); return { id: board.id, title: board.title, adminKey, memberKey, adminUrlFragment: `/b/${board.id}#admin=${adminKey}`, memberUrlFragment: `/b/${board.id}#member=${memberKey}`, createdAt: board.createdAt.toISOString(), }; } /** * Authenticate a key against a board ID. Returns the role ('admin' | 'member') or null if invalid. */ static async authenticateKey(boardId, key) { if (!boardId || !key) return null; const board = await prisma.board.findUnique({ where: { id: boardId }, }); if (!board) return null; if (verifyKeyHash(key, board.adminKeyHash)) { return { role: 'admin', board }; } if (verifyKeyHash(key, board.memberKeyHash)) { return { role: 'member', board }; } return null; } /** * Fetch full board data with columns and tickets ordered properly. */ static async getBoardDetails(boardId, role) { const board = await prisma.board.findUnique({ where: { id: boardId }, include: { columns: { orderBy: { order: 'asc' }, include: { tickets: { orderBy: { order: 'asc' }, }, }, }, }, }); if (!board) return null; const columns = board.columns.map((col) => ({ id: col.id, boardId: col.boardId, title: col.title, order: col.order, maxWipLimit: col.maxWipLimit, autoStaleHours: col.autoStaleHours, createdAt: col.createdAt.toISOString(), updatedAt: col.updatedAt.toISOString(), tickets: col.tickets.map((t) => { let tags = []; let subtasks = []; try { tags = JSON.parse(t.tags || '[]'); } catch { tags = []; } try { subtasks = JSON.parse(t.subtasks || '[]'); } catch { subtasks = []; } return { id: t.id, columnId: t.columnId, title: t.title, description: t.description, order: t.order, priority: t.priority, tags, subtasks, dueDate: t.dueDate || null, isBlocked: t.isBlocked || false, blockedReason: t.blockedReason || null, assignee: t.assignee, movedAt: t.movedAt ? t.movedAt.toISOString() : t.createdAt.toISOString(), createdAt: t.createdAt.toISOString(), updatedAt: t.updatedAt.toISOString(), }; }), })); let memberKeyVal = board.memberKey || ''; if (!memberKeyVal && role === 'admin') { const newMemberKey = generateSecureKey(32); const newMemberKeyHash = hashKey(newMemberKey); await prisma.board.update({ where: { id: boardId }, data: { memberKey: newMemberKey, memberKeyHash: newMemberKeyHash, }, }); memberKeyVal = newMemberKey; } return { id: board.id, title: board.title, role, memberKey: memberKeyVal, memberUrlFragment: memberKeyVal ? `/b/${board.id}#member=${memberKeyVal}` : '', autoMoveStale: board.autoMoveStale || false, columns, createdAt: board.createdAt.toISOString(), updatedAt: board.updatedAt.toISOString(), }; } /** * Regenerate member key (Key Rotation). Only superadmin can perform this. */ static async rotateMemberKey(boardId) { const newMemberKey = generateSecureKey(32); const newMemberKeyHash = hashKey(newMemberKey); await prisma.board.update({ where: { id: boardId }, data: { memberKeyHash: newMemberKeyHash, memberKey: newMemberKey, }, }); return { newMemberKey, memberUrlFragment: `/b/${boardId}#member=${newMemberKey}`, }; } /** * Delete a board. Only superadmin can perform this. */ static async deleteBoard(boardId) { await prisma.board.delete({ where: { id: boardId }, }); } /** * Update board title & settings. */ static async updateBoard(boardId, updates) { const data = {}; if (updates.title !== undefined) data.title = updates.title.trim() || 'Agile Kanban Board'; if (updates.autoMoveStale !== undefined) data.autoMoveStale = updates.autoMoveStale; await prisma.board.update({ where: { id: boardId }, data, }); } }