This commit addresses a critical bug where subtasks would disappear when updating the parent task (e.g., assigning tags). The issue had multiple potential causes: 1. Backend vulnerability: The updateSubtasks() function would delete all subtasks if an empty array was sent, treating it as "delete everything not in this list" 2. Frontend state management: After reloading a task, subtasks weren't being preserved if the backend response didn't include them 3. Unclear error messages: "Invalid parent task" errors didn't provide enough context for debugging Changes: - Added defensive logging in updateSubtasks() to warn when all subtasks are being deleted with an empty array - Enhanced validateParentTaskAccess() error messages to provide detailed diagnostics (task not found vs. permission issues vs. wrong user) - Updated handleTagsUpdate() in TaskDetails to explicitly preserve subtasks when reloading task after tag updates This fix is defensive in nature and adds better observability for diagnosing similar issues in the future. Fixes issue reported by user where subtasks disappeared after assigning tags to parent task, and "Invalid parent task" errors occurred when trying to update the orphaned subtasks.
139 lines
4.5 KiB
JavaScript
139 lines
4.5 KiB
JavaScript
const { Project, Task } = require('../../../models');
|
|
const permissionsService = require('../../../services/permissionsService');
|
|
|
|
async function validateProjectAccess(projectId, userId) {
|
|
if (!projectId || !projectId.toString().trim()) {
|
|
return null;
|
|
}
|
|
|
|
const project = await Project.findOne({ where: { id: projectId } });
|
|
if (!project) {
|
|
throw new Error('Invalid project.');
|
|
}
|
|
|
|
const projectAccess = await permissionsService.getAccess(
|
|
userId,
|
|
'project',
|
|
project.uid
|
|
);
|
|
const isOwner = project.user_id === userId;
|
|
const canWrite =
|
|
isOwner || projectAccess === 'rw' || projectAccess === 'admin';
|
|
|
|
if (!canWrite) {
|
|
throw new Error('Forbidden');
|
|
}
|
|
|
|
return projectId;
|
|
}
|
|
|
|
async function validateParentTaskAccess(parentTaskId, userId) {
|
|
if (!parentTaskId || !parentTaskId.toString().trim()) {
|
|
return null;
|
|
}
|
|
|
|
const parentTask = await Task.findOne({
|
|
where: { id: parentTaskId, user_id: userId },
|
|
});
|
|
if (!parentTask) {
|
|
const anyTask = await Task.findOne({
|
|
where: { id: parentTaskId },
|
|
});
|
|
if (anyTask) {
|
|
throw new Error(
|
|
`Invalid parent task. Parent task exists but belongs to a different user (parent user_id: ${anyTask.user_id}, current user_id: ${userId}).`
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`Invalid parent task. Parent task with id ${parentTaskId} not found.`
|
|
);
|
|
}
|
|
}
|
|
|
|
const parentAccess = await permissionsService.getAccess(
|
|
userId,
|
|
'task',
|
|
parentTask.uid
|
|
);
|
|
const isOwner = parentTask.user_id === userId;
|
|
const canWrite =
|
|
isOwner || parentAccess === 'rw' || parentAccess === 'admin';
|
|
|
|
if (!canWrite) {
|
|
throw new Error('Invalid parent task. Insufficient permissions.');
|
|
}
|
|
|
|
return parentTaskId;
|
|
}
|
|
|
|
/**
|
|
* Validates that defer_until date is not after the due_date for regular tasks,
|
|
* or after the recurrence_end_date for recurring task instances.
|
|
*
|
|
* @param {string|Date|null} deferUntil - The defer until date
|
|
* @param {string|Date|null} dueDate - The task due date
|
|
* @param {string|Date|null|undefined} recurringParentEndDate - The parent task's recurrence end date
|
|
* undefined = not a recurring instance (apply strict validation)
|
|
* null = recurring instance with no end date (allow any defer_until)
|
|
* date = recurring instance with end date (validate against end date)
|
|
* @throws {Error} If defer_until is after the applicable end date
|
|
*
|
|
* Validation rules:
|
|
* - If no defer_until or due_date: validation passes
|
|
* - If recurringParentEndDate is undefined (not provided): regular task, defer_until must be <= due_date
|
|
* - If recurringParentEndDate is null: infinite recurrence, any defer_until is allowed
|
|
* - If recurringParentEndDate is a date: defer_until must be <= end date
|
|
*/
|
|
function validateDeferUntilAndDueDate(
|
|
deferUntil,
|
|
dueDate,
|
|
recurringParentEndDate = undefined
|
|
) {
|
|
// Both must be present to validate
|
|
if (!deferUntil || !dueDate) {
|
|
return;
|
|
}
|
|
|
|
const deferDate = new Date(deferUntil);
|
|
const dueDateObj = new Date(dueDate);
|
|
|
|
// Check if dates are valid
|
|
if (isNaN(deferDate.getTime()) || isNaN(dueDateObj.getTime())) {
|
|
return;
|
|
}
|
|
|
|
// Check if this is a recurring instance (parameter was explicitly passed)
|
|
if (recurringParentEndDate !== undefined) {
|
|
// If parent has null end date, it's infinite recurrence - allow any defer_until
|
|
if (recurringParentEndDate === null) {
|
|
return;
|
|
}
|
|
|
|
// Parent has an end date - validate against it
|
|
const endDate = new Date(recurringParentEndDate);
|
|
if (!isNaN(endDate.getTime())) {
|
|
if (deferDate > endDate) {
|
|
throw new Error(
|
|
'Defer until date cannot be after the recurring task end date.'
|
|
);
|
|
}
|
|
// Validation passes - defer can be after due_date but within recurrence bounds
|
|
return;
|
|
}
|
|
|
|
// Invalid end date but has parent - treat as infinite recurrence
|
|
return;
|
|
}
|
|
|
|
// Not a recurring instance - apply strict validation
|
|
// Defer until must be before or equal to due date
|
|
if (deferDate > dueDateObj) {
|
|
throw new Error('Defer until date cannot be after the due date.');
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
validateProjectAccess,
|
|
validateParentTaskAccess,
|
|
validateDeferUntilAndDueDate,
|
|
};
|