diff --git a/PROGRESS.md b/PROGRESS.md index f856b48..72ac627 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -70,6 +70,7 @@ - **`Escape`**: Instantly close any open modal (Task Detail, Add Task, Settings, or Audit Logs). - **`n`**: Open the "Add Task" modal from the main dashboard (disabled while typing in inputs). 24. **Drag-and-Drop Stability:** Fixed a bug where tasks would "disappear" if dropped in an invalid area. Tasks now remain visible at their original position if a drop is cancelled. +25. **User Deletion Safety:** Implemented a backend check to prevent deleting users who have assigned tasks or notes. Upgraded the frontend `ApiService` to correctly parse and display these descriptive error messages from the backend. ### Phase 3: Advanced Features - **Real-time Notifications:** Explore WebSockets for task assignments. diff --git a/api.js b/api.js index 1f2a97b..5883705 100644 --- a/api.js +++ b/api.js @@ -32,7 +32,12 @@ class ApiService { if (response.status === 401) { this.logout(); } - throw new Error(`API Error: ${response.statusText}`); + let errorMsg = response.statusText; + try { + const data = await response.json(); + if (data && data.detail) errorMsg = data.detail; + } catch (e) {} + throw new Error(`API Error: ${errorMsg}`); } return response.json(); diff --git a/app.jsx b/app.jsx index 8d9a169..0784ad0 100644 --- a/app.jsx +++ b/app.jsx @@ -392,7 +392,7 @@ function App() { await api.addAudit({ actor: meId, action: 'user_deleted', summary: 'Removed ' + (u?u.name:id), target: null }); } catch(e) { console.error(e); - alert("Failed to delete user"); + alert("Failed to delete user: " + e.message); } }} onUpdateUserRole={async (id, edits) => { @@ -401,7 +401,7 @@ function App() { await api.addAudit({ actor: meId, action: 'user_updated', summary: 'Updated ' + (userMap[id]?userMap[id].name:id) + ' permissions', target: null }); } catch(e) { console.error(e); - alert("Failed to update user"); + alert("Failed to update user: " + e.message); } }} onChangePassword={async (oldPwd, newPwd) => { diff --git a/backend/main.py b/backend/main.py index fac61fd..3d8e754 100644 --- a/backend/main.py +++ b/backend/main.py @@ -96,6 +96,19 @@ def delete_user(user_id: str, db: Session = Depends(get_db), current_user: model db_user = db.query(models.User).filter(models.User.id == user_id).first() if not db_user: raise HTTPException(status_code=404, detail="User not found") + + # Check for OPEN assigned tasks (that are not in the trash) + open_tasks = db.query(models.Task).filter( + models.Task.assignee_id == user_id, + models.Task.status == "open", + models.Task.deleted_at == None + ).first() + if open_tasks: + raise HTTPException(status_code=400, detail="Cannot delete user: They still have OPEN tasks assigned to them. Reassign them first.") + + # Nullify references in closed tasks and notes so we don't lose history + db.query(models.Task).filter(models.Task.assignee_id == user_id).update({"assignee_id": None}) + db.query(models.TaskNote).filter(models.TaskNote.author_id == user_id).update({"author_id": None}) db.delete(db_user) db.commit() diff --git a/backend/models.py b/backend/models.py index a6e1854..99bf66a 100644 --- a/backend/models.py +++ b/backend/models.py @@ -59,7 +59,7 @@ class TaskNote(Base): id = Column(Integer, primary_key=True, index=True) task_id = Column(String, ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False) - author_id = Column(String, ForeignKey("users.id"), nullable=False) + author_id = Column(String, ForeignKey("users.id"), nullable=True) body = Column(String, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/schemas.py b/backend/schemas.py index c28f98a..0ccd8c6 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -62,7 +62,7 @@ class Tag(TagBase): class TaskBase(BaseModel): title: str description: Optional[str] = None - assignee_id: str + assignee_id: Optional[str] = None added_by: str priority: str source: str diff --git a/dashy.db b/dashy.db index f7842e7..e2a6cab 100644 Binary files a/dashy.db and b/dashy.db differ