notes,tasks: standardize forbidden responses and centralize access checks
- Add hasAccess middleware to note/task GET by id endpoints - Ensure 403 Forbidden when user lacks access to existing resources - Preserve 404 for genuinely missing IDs - Update list queries to include shared items via ownershipOrPermissionWhere - Align subtasks endpoint to return 403 when parent not accessible - Update integration tests accordingly
This commit is contained in:
parent
11a72ced72
commit
a24a4688eb
5 changed files with 269 additions and 106 deletions
|
|
@ -4,6 +4,8 @@ const { Op } = require('sequelize');
|
|||
const { extractUidFromSlug } = require('../utils/slug-utils');
|
||||
const { validateTagName } = require('../utils/validation');
|
||||
const router = express.Router();
|
||||
const permissionsService = require('../services/permissionsService');
|
||||
const { hasAccess } = require('../middleware/authorize');
|
||||
|
||||
// Helper function to update note tags
|
||||
async function updateNoteTags(note, tagsArray, userId) {
|
||||
|
|
@ -62,7 +64,10 @@ router.get('/notes', async (req, res) => {
|
|||
const orderBy = req.query.order_by || 'title:asc';
|
||||
const [orderColumn, orderDirection] = orderBy.split(':');
|
||||
|
||||
let whereClause = { user_id: req.session.userId };
|
||||
const whereClause = await permissionsService.ownershipOrPermissionWhere(
|
||||
'note',
|
||||
req.session.userId
|
||||
);
|
||||
let includeClause = [
|
||||
{
|
||||
model: Tag,
|
||||
|
|
@ -97,31 +102,34 @@ router.get('/notes', async (req, res) => {
|
|||
});
|
||||
|
||||
// GET /api/note/:id (supports both numeric ID and uid-slug)
|
||||
router.get('/note/:id', async (req, res) => {
|
||||
router.get(
|
||||
'/note/:id',
|
||||
hasAccess(
|
||||
'ro',
|
||||
'note',
|
||||
async (req) => {
|
||||
const identifier = req.params.id;
|
||||
if (/^\d+$/.test(identifier)) {
|
||||
const n = await Note.findOne({ where: { id: parseInt(identifier) }, attributes: ['uid'] });
|
||||
return n?.uid;
|
||||
}
|
||||
const uid = extractUidFromSlug(identifier);
|
||||
return uid;
|
||||
},
|
||||
{ notFoundMessage: 'Note not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
if (!req.session || !req.session.userId) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
const identifier = req.params.id;
|
||||
let whereClause;
|
||||
|
||||
// Check if identifier is numeric (regular ID) or uid-slug
|
||||
if (/^\d+$/.test(identifier)) {
|
||||
// It's a numeric ID
|
||||
whereClause = {
|
||||
id: parseInt(identifier),
|
||||
user_id: req.session.userId,
|
||||
};
|
||||
whereClause = { id: parseInt(identifier) };
|
||||
} else {
|
||||
// It's a uid-slug, extract the uid
|
||||
const uid = extractUidFromSlug(identifier);
|
||||
if (!uid) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid note identifier' });
|
||||
return res.status(400).json({ error: 'Invalid note identifier' });
|
||||
}
|
||||
whereClause = { uid: uid, user_id: req.session.userId };
|
||||
whereClause = { uid };
|
||||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
|
|
@ -143,13 +151,15 @@ router.get('/note/:id', async (req, res) => {
|
|||
if (!note) {
|
||||
return res.status(404).json({ error: 'Note not found.' });
|
||||
}
|
||||
// access ensured by middleware
|
||||
|
||||
res.json(note);
|
||||
} catch (error) {
|
||||
console.error('Error fetching note:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/note
|
||||
router.post('/note', async (req, res) => {
|
||||
|
|
@ -168,12 +178,20 @@ router.post('/note', async (req, res) => {
|
|||
|
||||
// Handle project assignment
|
||||
if (project_id && project_id.toString().trim()) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: project_id, user_id: req.session.userId },
|
||||
});
|
||||
const project = await Project.findOne({ where: { id: project_id } });
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Invalid project.' });
|
||||
}
|
||||
const projectAccess = await permissionsService.getAccess(
|
||||
req.session.userId,
|
||||
'project',
|
||||
project.uid
|
||||
);
|
||||
const isOwner = project.user_id === req.session.userId;
|
||||
const canWrite = isOwner || projectAccess === 'rw' || projectAccess === 'admin';
|
||||
if (!canWrite) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
noteAttributes.project_id = project_id;
|
||||
}
|
||||
|
||||
|
|
@ -223,19 +241,27 @@ router.post('/note', async (req, res) => {
|
|||
});
|
||||
|
||||
// PATCH /api/note/:id
|
||||
router.patch('/note/:id', async (req, res) => {
|
||||
router.patch(
|
||||
'/note/:id',
|
||||
hasAccess(
|
||||
'rw',
|
||||
'note',
|
||||
async (req) => {
|
||||
const n = await Note.findOne({ where: { id: req.params.id }, attributes: ['uid'] });
|
||||
return n?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Note not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
if (!req.session || !req.session.userId) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).json({ error: 'Note not found.' });
|
||||
}
|
||||
// access ensured by middleware
|
||||
|
||||
const { title, content, project_id, tags } = req.body;
|
||||
|
||||
|
|
@ -246,12 +272,20 @@ router.patch('/note/:id', async (req, res) => {
|
|||
// Handle project assignment
|
||||
if (project_id !== undefined) {
|
||||
if (project_id && project_id.toString().trim()) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: project_id, user_id: req.session.userId },
|
||||
});
|
||||
const project = await Project.findOne({ where: { id: project_id } });
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Invalid project.' });
|
||||
}
|
||||
const projectAccess = await permissionsService.getAccess(
|
||||
req.session.userId,
|
||||
'project',
|
||||
project.uid
|
||||
);
|
||||
const isOwner = project.user_id === req.session.userId;
|
||||
const canWrite = isOwner || projectAccess === 'rw' || projectAccess === 'admin';
|
||||
if (!canWrite) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
updateData.project_id = project_id;
|
||||
} else {
|
||||
updateData.project_id = null;
|
||||
|
|
@ -299,22 +333,31 @@ router.patch('/note/:id', async (req, res) => {
|
|||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE /api/note/:id
|
||||
router.delete('/note/:id', async (req, res) => {
|
||||
router.delete(
|
||||
'/note/:id',
|
||||
hasAccess(
|
||||
'rw',
|
||||
'note',
|
||||
async (req) => {
|
||||
const n = await Note.findOne({ where: { id: req.params.id }, attributes: ['uid'] });
|
||||
return n?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Note not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
if (!req.session || !req.session.userId) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).json({ error: 'Note not found.' });
|
||||
}
|
||||
// access ensured by middleware
|
||||
|
||||
await note.destroy();
|
||||
res.json({ message: 'Note deleted successfully.' });
|
||||
|
|
@ -324,6 +367,7 @@ router.delete('/note/:id', async (req, res) => {
|
|||
error: 'There was a problem deleting the note.',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const express = require('express');
|
||||
const { Task, Tag, Project, TaskEvent, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const permissionsService = require('../services/permissionsService');
|
||||
const { hasAccess } = require('../middleware/authorize');
|
||||
const {
|
||||
generateRecurringTasks,
|
||||
handleTaskCompletion,
|
||||
|
|
@ -454,16 +456,19 @@ async function undoneAllSubtasks(parentTaskId, userId) {
|
|||
|
||||
// Filter tasks by parameters
|
||||
async function filterTasksByParams(params, userId, userTimezone) {
|
||||
// Disable search functionality for upcoming view
|
||||
if (params.type === 'upcoming') {
|
||||
// Include owned or shared tasks; exclude subtasks by default
|
||||
const ownedOrShared = await permissionsService.ownershipOrPermissionWhere(
|
||||
'task',
|
||||
'userId'
|
||||
);
|
||||
if (params.type === 'upcoming') {
|
||||
// Remove search-related parameters to prevent search functionality
|
||||
params = { ...params, client_side_filtering: false };
|
||||
delete params.search;
|
||||
}
|
||||
|
||||
let whereClause = {
|
||||
user_id: userId,
|
||||
parent_task_id: null, // Exclude subtasks from main task lists
|
||||
...ownedOrShared,
|
||||
parent_task_id: null,
|
||||
};
|
||||
|
||||
// Include both recurring templates and instances, but handle them appropriately
|
||||
|
|
@ -567,7 +572,7 @@ async function filterTasksByParams(params, userId, userTimezone) {
|
|||
// Override the default whereClause to include recurring instances
|
||||
// NOTE: Search functionality is disabled for upcoming view - ignore client_side_filtering
|
||||
whereClause = {
|
||||
user_id: userId,
|
||||
...ownedOrShared,
|
||||
parent_task_id: null, // Exclude subtasks from main task lists
|
||||
due_date: {
|
||||
[Op.between]: [upcomingRange.start, upcomingRange.end],
|
||||
|
|
@ -699,19 +704,19 @@ async function filterTasksByParams(params, userId, userTimezone) {
|
|||
|
||||
// Compute task metrics
|
||||
async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
||||
const visibleTasksWhere = await permissionsService.ownershipOrPermissionWhere('task', userId);
|
||||
const totalOpenTasks = await Task.count({
|
||||
where: {
|
||||
user_id: userId,
|
||||
where: { ...visibleTasksWhere,
|
||||
status: { [Op.ne]: Task.STATUS.DONE },
|
||||
parent_task_id: null, // Exclude subtasks
|
||||
recurring_parent_id: null, // Exclude recurring instances
|
||||
},
|
||||
});
|
||||
parent_task_id: null, // Exclude subtasks
|
||||
recurring_parent_id: null, // Exclude recurring instances
|
||||
}
|
||||
});
|
||||
|
||||
const oneMonthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const tasksPendingOverMonth = await Task.count({
|
||||
where: {
|
||||
user_id: userId,
|
||||
...visibleTasksWhere,
|
||||
status: { [Op.ne]: Task.STATUS.DONE },
|
||||
created_at: { [Op.lt]: oneMonthAgo },
|
||||
parent_task_id: null, // Exclude subtasks
|
||||
|
|
@ -721,7 +726,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
|
||||
const tasksInProgress = await Task.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
...visibleTasksWhere,
|
||||
status: { [Op.in]: [Task.STATUS.IN_PROGRESS, 'in_progress'] },
|
||||
parent_task_id: null, // Exclude subtasks
|
||||
recurring_parent_id: null, // Exclude recurring instances
|
||||
|
|
@ -758,7 +763,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
// Get tasks in today plan
|
||||
const todayPlanTasks = await Task.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
...visibleTasksWhere,
|
||||
today: true,
|
||||
status: {
|
||||
[Op.notIn]: [
|
||||
|
|
@ -809,7 +814,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
|
||||
const tasksDueToday = await Task.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
...visibleTasksWhere,
|
||||
status: {
|
||||
[Op.notIn]: [
|
||||
Task.STATUS.DONE,
|
||||
|
|
@ -889,7 +894,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
// Get tasks without projects (excluding someday tagged tasks)
|
||||
const nonProjectTasks = await Task.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
...visibleTasksWhere,
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.NOT_STARTED, Task.STATUS.WAITING],
|
||||
},
|
||||
|
|
@ -934,7 +939,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
// Get tasks with projects (excluding someday tagged tasks)
|
||||
const projectTasks = await Task.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
...visibleTasksWhere,
|
||||
status: {
|
||||
[Op.in]: [Task.STATUS.NOT_STARTED, Task.STATUS.WAITING],
|
||||
},
|
||||
|
|
@ -1315,8 +1320,10 @@ router.get('/task', async (req, res) => {
|
|||
.json({ error: 'uid query parameter is required' });
|
||||
}
|
||||
|
||||
const task = await Task.findOne({
|
||||
where: { uid: uid, user_id: req.currentUser.id },
|
||||
const task = await Task.findOne({
|
||||
where: {
|
||||
uid: uid,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
|
|
@ -1347,6 +1354,16 @@ router.get('/task', async (req, res) => {
|
|||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
|
||||
// Ensure read access to the task
|
||||
const access = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'task',
|
||||
task.uid
|
||||
);
|
||||
if (access === 'none') {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
const serializedTask = await serializeTask(
|
||||
task,
|
||||
req.currentUser.timezone
|
||||
|
|
@ -1360,10 +1377,24 @@ router.get('/task', async (req, res) => {
|
|||
});
|
||||
|
||||
// GET /api/task/:id
|
||||
router.get('/task/:id', async (req, res) => {
|
||||
router.get(
|
||||
'/task/:id',
|
||||
hasAccess(
|
||||
'ro',
|
||||
'task',
|
||||
async (req) => {
|
||||
const t = await Task.findOne({
|
||||
where: { id: req.params.id },
|
||||
attributes: ['uid'],
|
||||
});
|
||||
return t?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Task not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const task = await Task.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
where: { id: req.params.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
|
|
@ -1389,9 +1420,9 @@ router.get('/task/:id', async (req, res) => {
|
|||
],
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
|
||||
const serializedTask = await serializeTask(
|
||||
task,
|
||||
|
|
@ -1399,20 +1430,34 @@ router.get('/task/:id', async (req, res) => {
|
|||
{ skipDisplayNameTransform: true }
|
||||
);
|
||||
|
||||
res.json(serializedTask);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
res.json(serializedTask);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// GET /api/task/:id/subtasks
|
||||
router.get('/task/:id/subtasks', async (req, res) => {
|
||||
try {
|
||||
// Ensure parent visibility first
|
||||
const parent = await Task.findOne({ where: { id: req.params.id } });
|
||||
if (!parent) {
|
||||
return res.json([]);
|
||||
}
|
||||
const pAccess = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'task',
|
||||
parent.uid
|
||||
);
|
||||
if (pAccess === 'none') {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
const subtasks = await Task.findAll({
|
||||
where: {
|
||||
parent_task_id: req.params.id,
|
||||
user_id: req.currentUser.id,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
|
|
@ -1513,23 +1558,39 @@ router.post('/task', async (req, res) => {
|
|||
|
||||
// Handle project assignment
|
||||
if (project_id && project_id.toString().trim()) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: project_id, user_id: req.currentUser.id },
|
||||
});
|
||||
const project = await Project.findOne({ where: { id: project_id } });
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Invalid project.' });
|
||||
}
|
||||
const projectAccess = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'project',
|
||||
project.uid
|
||||
);
|
||||
const isOwner = project.user_id === req.currentUser.id;
|
||||
const canWrite = isOwner || projectAccess === 'rw' || projectAccess === 'admin';
|
||||
if (!canWrite) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
taskAttributes.project_id = project_id;
|
||||
}
|
||||
|
||||
// Handle parent task assignment
|
||||
if (parent_task_id && parent_task_id.toString().trim()) {
|
||||
const parentTask = await Task.findOne({
|
||||
where: { id: parent_task_id, user_id: req.currentUser.id },
|
||||
});
|
||||
const parentTask = await Task.findOne({ where: { id: parent_task_id } });
|
||||
if (!parentTask) {
|
||||
return res.status(400).json({ error: 'Invalid parent task.' });
|
||||
}
|
||||
const parentAccess = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'task',
|
||||
parentTask.uid
|
||||
);
|
||||
const isOwner = parentTask.user_id === req.currentUser.id;
|
||||
const canWrite = isOwner || parentAccess === 'rw' || parentAccess === 'admin';
|
||||
if (!canWrite) {
|
||||
return res.status(400).json({ error: 'Invalid parent task.' });
|
||||
}
|
||||
taskAttributes.parent_task_id = parent_task_id;
|
||||
}
|
||||
|
||||
|
|
@ -1644,7 +1705,18 @@ router.post('/task', async (req, res) => {
|
|||
});
|
||||
|
||||
// PATCH /api/task/:id
|
||||
router.patch('/task/:id', async (req, res) => {
|
||||
router.patch(
|
||||
'/task/:id',
|
||||
hasAccess(
|
||||
'rw',
|
||||
'task',
|
||||
async (req) => {
|
||||
const t = await Task.findOne({ where: { id: req.params.id }, attributes: ['uid'] });
|
||||
return t?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Task not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
|
|
@ -1672,7 +1744,7 @@ router.patch('/task/:id', async (req, res) => {
|
|||
const tagsData = tags || Tags;
|
||||
|
||||
const task = await Task.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
where: { id: req.params.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
|
|
@ -1685,6 +1757,7 @@ router.patch('/task/:id', async (req, res) => {
|
|||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
// access ensured by middleware
|
||||
|
||||
// Capture old values for event logging
|
||||
const oldValues = {
|
||||
|
|
@ -1837,12 +1910,20 @@ router.patch('/task/:id', async (req, res) => {
|
|||
|
||||
// Handle project assignment
|
||||
if (project_id && project_id.toString().trim()) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: project_id, user_id: req.currentUser.id },
|
||||
});
|
||||
const project = await Project.findOne({ where: { id: project_id } });
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Invalid project.' });
|
||||
}
|
||||
const projectAccess = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'project',
|
||||
project.uid
|
||||
);
|
||||
const isOwner = project.user_id === req.currentUser.id;
|
||||
const canWrite = isOwner || projectAccess === 'rw' || projectAccess === 'admin';
|
||||
if (!canWrite) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
taskAttributes.project_id = project_id;
|
||||
} else {
|
||||
taskAttributes.project_id = null;
|
||||
|
|
@ -2234,13 +2315,25 @@ router.patch('/task/:id', async (req, res) => {
|
|||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// PATCH /api/task/:id/toggle_completion
|
||||
router.patch('/task/:id/toggle_completion', async (req, res) => {
|
||||
router.patch(
|
||||
'/task/:id/toggle_completion',
|
||||
hasAccess(
|
||||
'rw',
|
||||
'task',
|
||||
async (req) => {
|
||||
const t = await Task.findOne({ where: { id: req.params.id }, attributes: ['uid'] });
|
||||
return t?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Task not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const task = await Task.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
where: { id: req.params.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
|
|
@ -2269,6 +2362,7 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
// access ensured by middleware
|
||||
|
||||
// Track if parent-child logic was executed
|
||||
let parentChildLogicExecuted = false;
|
||||
|
|
@ -2383,13 +2477,25 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
details: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE /api/task/:id
|
||||
router.delete('/task/:id', async (req, res) => {
|
||||
router.delete(
|
||||
'/task/:id',
|
||||
hasAccess(
|
||||
'rw',
|
||||
'task',
|
||||
async (req) => {
|
||||
const t = await Task.findOne({ where: { id: req.params.id }, attributes: ['uid'] });
|
||||
return t?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Task not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const task = await Task.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
|
|
@ -2516,7 +2622,8 @@ router.delete('/task/:id', async (req, res) => {
|
|||
error: 'There was a problem deleting the task.',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/tasks/generate-recurring
|
||||
router.post('/tasks/generate-recurring', async (req, res) => {
|
||||
|
|
@ -2539,10 +2646,21 @@ router.post('/tasks/generate-recurring', async (req, res) => {
|
|||
});
|
||||
|
||||
// PATCH /api/task/:id/toggle-today
|
||||
router.patch('/task/:id/toggle-today', async (req, res) => {
|
||||
router.patch(
|
||||
'/task/:id/toggle-today',
|
||||
hasAccess(
|
||||
'rw',
|
||||
'task',
|
||||
async (req) => {
|
||||
const t = await Task.findOne({ where: { id: req.params.id }, attributes: ['uid'] });
|
||||
return t?.uid;
|
||||
},
|
||||
{ notFoundMessage: 'Task not found.' }
|
||||
),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const task = await Task.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
where: { id: req.params.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
|
|
@ -2561,6 +2679,8 @@ router.patch('/task/:id/toggle-today', async (req, res) => {
|
|||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
|
||||
// access ensured by middleware
|
||||
|
||||
// Toggle the today flag
|
||||
const newTodayValue = !task.today;
|
||||
const updateData = { today: newTodayValue };
|
||||
|
|
@ -2601,7 +2721,8 @@ router.patch('/task/:id/toggle-today', async (req, res) => {
|
|||
console.error('Error toggling task today flag:', error);
|
||||
res.status(500).json({ error: 'Failed to update task today flag' });
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/task/:id/next-iterations
|
||||
router.get('/task/:id/next-iterations', async (req, res) => {
|
||||
|
|
|
|||
|
|
@ -169,8 +169,8 @@ describe('Notes Routes', () => {
|
|||
|
||||
const response = await agent.get(`/api/note/${otherNote.id}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Note not found.');
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
@ -234,8 +234,8 @@ describe('Notes Routes', () => {
|
|||
.patch(`/api/note/${otherNote.id}`)
|
||||
.send({ title: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Note not found.');
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
@ -290,8 +290,8 @@ describe('Notes Routes', () => {
|
|||
|
||||
const response = await agent.delete(`/api/note/${otherNote.id}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Note not found.');
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ describe('Subtasks API', () => {
|
|||
});
|
||||
|
||||
describe('Authentication and Authorization', () => {
|
||||
it('should not allow access to subtasks of other users', async () => {
|
||||
it('should return 403 when accessing subtasks of other users', async () => {
|
||||
const otherUser = await createTestUser({
|
||||
email: `other_${Date.now()}@example.com`,
|
||||
});
|
||||
|
|
@ -627,12 +627,10 @@ describe('Subtasks API', () => {
|
|||
priority: Task.PRIORITY.MEDIUM,
|
||||
});
|
||||
|
||||
// API returns empty array for other users' tasks instead of 401
|
||||
const response = await agent
|
||||
.get(`/api/task/${otherTask.id}/subtasks`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveLength(0);
|
||||
.expect(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('should not allow creating subtasks for other users tasks', async () => {
|
||||
|
|
|
|||
|
|
@ -201,8 +201,8 @@ describe('Tasks Routes', () => {
|
|||
.patch(`/api/task/${otherTask.id}`)
|
||||
.send({ name: 'Updated' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Task not found.');
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
@ -257,8 +257,8 @@ describe('Tasks Routes', () => {
|
|||
|
||||
const response = await agent.delete(`/api/task/${otherTask.id}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Task not found.');
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
}, 10000); // 10 second timeout for this specific test
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue