first init
This commit is contained in:
Vendored
+169
@@ -0,0 +1,169 @@
|
||||
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,
|
||||
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 || 0,
|
||||
autoStaleHours: col.autoStaleHours || 0,
|
||||
createdAt: col.createdAt.toISOString(),
|
||||
updatedAt: col.updatedAt.toISOString(),
|
||||
tickets: col.tickets.map((t) => {
|
||||
let parsedTags = [];
|
||||
let parsedSubtasks = [];
|
||||
try {
|
||||
parsedTags = JSON.parse(t.tags || '[]');
|
||||
}
|
||||
catch {
|
||||
parsedTags = [];
|
||||
}
|
||||
try {
|
||||
parsedSubtasks = JSON.parse(t.subtasks || '[]');
|
||||
}
|
||||
catch {
|
||||
parsedSubtasks = [];
|
||||
}
|
||||
return {
|
||||
id: t.id,
|
||||
columnId: t.columnId,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
order: t.order,
|
||||
priority: t.priority,
|
||||
tags: parsedTags,
|
||||
subtasks: parsedSubtasks,
|
||||
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(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
return {
|
||||
id: board.id,
|
||||
title: board.title,
|
||||
role,
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user