187 lines
6.6 KiB
JavaScript
187 lines
6.6 KiB
JavaScript
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,
|
|
});
|
|
}
|
|
}
|