From 56f42a293cc8593e1d2dba17989ff03dd9ec22fa Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 26 Mar 2026 16:14:35 +0100 Subject: [PATCH] fix: exclude soft-deleted projects from all raw SQL queries Add deleted_at IS NULL filters to: - getUserProjectsStatement (project listing base query) - getAllProjectsForUser recursive CTE - GetAllParentProjects recursive CTE - setArchiveStateForProjectDescendants CTE - checkPermissionsForProjects permission resolver CTE - Task overdue reminders JOIN - Subscription CTEs (project and task) - Task search parent_project sub-table filters - ListUsersFromProject query - RepairOrphanedProjects (exclude soft-deleted from orphan detection) --- pkg/models/project_permissions.go | 5 +++-- pkg/models/project_repair.go | 4 +++- pkg/models/subscription.go | 6 ++++-- pkg/models/task_overdue_reminder.go | 2 +- pkg/models/task_search.go | 4 ++-- pkg/models/user_project.go | 1 + 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/models/project_permissions.go b/pkg/models/project_permissions.go index ea22983af..fde169399 100644 --- a/pkg/models/project_permissions.go +++ b/pkg/models/project_permissions.go @@ -261,7 +261,7 @@ WITH RECURSIVE 0 AS level, id AS original_project_id FROM projects - WHERE id IN (`+utils.JoinInt64Slice(projectIDs, ", ")+`) + WHERE id IN (`+utils.JoinInt64Slice(projectIDs, ", ")+`) AND deleted_at IS NULL UNION ALL @@ -271,7 +271,8 @@ WITH RECURSIVE ph.level + 1, ph.original_project_id FROM projects p - INNER JOIN project_hierarchy ph ON p.id = ph.parent_project_id), + INNER JOIN project_hierarchy ph ON p.id = ph.parent_project_id + WHERE p.deleted_at IS NULL), -- Calculate max team permission for each project/user combination max_team_permissions AS ( diff --git a/pkg/models/project_repair.go b/pkg/models/project_repair.go index 49e641755..4afd38b3e 100644 --- a/pkg/models/project_repair.go +++ b/pkg/models/project_repair.go @@ -36,9 +36,11 @@ func RepairOrphanedProjects(s *xorm.Session, dryRun bool) (*RepairOrphanedProjec result := &RepairOrphanedProjectsResult{} var orphans []*Project + // Use raw SQL that includes soft-deleted parents to avoid false positives: + // a child of a soft-deleted parent is NOT orphaned. err := s.SQL(`SELECT p.* FROM projects p LEFT JOIN projects parent ON p.parent_project_id = parent.id - WHERE p.parent_project_id > 0 AND parent.id IS NULL`). + WHERE p.parent_project_id > 0 AND parent.id IS NULL AND p.deleted_at IS NULL`). Find(&orphans) if err != nil { return nil, err diff --git a/pkg/models/subscription.go b/pkg/models/subscription.go index ad664c23f..ff21e609d 100644 --- a/pkg/models/subscription.go +++ b/pkg/models/subscription.go @@ -252,7 +252,7 @@ WITH RECURSIVE project_hierarchy AS ( 0 AS level, id AS original_project_id FROM projects - WHERE id IN (`+entityIDString+`) + WHERE id IN (`+entityIDString+`) AND deleted_at IS NULL UNION ALL @@ -264,6 +264,7 @@ WITH RECURSIVE project_hierarchy AS ( ph.original_project_id FROM projects p INNER JOIN project_hierarchy ph ON p.id = ph.parent_project_id + WHERE p.deleted_at IS NULL ), subscription_hierarchy AS ( @@ -318,7 +319,7 @@ WITH RECURSIVE project_hierarchy AS ( t.id AS task_id FROM tasks t JOIN projects p ON t.project_id = p.id - WHERE t.id IN (`+entityIDString+`) + WHERE t.id IN (`+entityIDString+`) AND p.deleted_at IS NULL UNION ALL @@ -330,6 +331,7 @@ WITH RECURSIVE project_hierarchy AS ( ph.task_id FROM projects p INNER JOIN project_hierarchy ph ON p.id = ph.parent_project_id + WHERE p.deleted_at IS NULL ), subscription_hierarchy AS ( diff --git a/pkg/models/task_overdue_reminder.go b/pkg/models/task_overdue_reminder.go index e10ef126e..b0cea0db4 100644 --- a/pkg/models/task_overdue_reminder.go +++ b/pkg/models/task_overdue_reminder.go @@ -38,7 +38,7 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time, cond builder.Cond) (u var tasks []*Task err = s. - Where("due_date is not null AND due_date < ? AND projects.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)). + Where("due_date is not null AND due_date < ? AND projects.is_archived = false AND projects.deleted_at IS NULL", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)). Join("LEFT", "projects", "projects.id = tasks.project_id"). And("done = false"). Find(&tasks) diff --git a/pkg/models/task_search.go b/pkg/models/task_search.go index d4a39197c..d3e577aff 100644 --- a/pkg/models/task_search.go +++ b/pkg/models/task_search.go @@ -64,13 +64,13 @@ var subTableFilters = SubTableFilters{ }, "parent_project": { Table: "projects", - BaseFilter: "tasks.project_id = id", + BaseFilter: "tasks.project_id = id AND deleted_at IS NULL", FilterableField: "parent_project_id", AllowNullCheck: false, }, "parent_project_id": { Table: "projects", - BaseFilter: "tasks.project_id = id", + BaseFilter: "tasks.project_id = id AND deleted_at IS NULL", FilterableField: "parent_project_id", AllowNullCheck: false, }, diff --git a/pkg/models/user_project.go b/pkg/models/user_project.go index 34ea85abe..7ae5eb465 100644 --- a/pkg/models/user_project.go +++ b/pkg/models/user_project.go @@ -66,6 +66,7 @@ func ListUsersFromProject(s *xorm.Session, l *Project, currentUser *user.User, s builder.Or(builder.Eq{"tl.permission": PermissionAdmin}), ), builder.Eq{"l.id": currentProject.ID}, + builder.IsNull{"l.deleted_at"}, ). Find(¤tUserIDs) if err != nil {