tududi/backend/modules/tasks/utils/validation.js
Chris aa686702e9
Fix defer date validation for recurring task instances (#910)
* Fix defer date validation for recurring task instances

* fixup! Fix defer date validation for recurring task instances
2026-03-05 08:20:30 +02:00

128 lines
4 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) {
throw new Error('Invalid parent task.');
}
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.');
}
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,
};