From 84900017eb2488636085a93349566f464a6297ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:46:20 +0000 Subject: [PATCH 1/8] Initial plan From e3c2233fe285ad7e0a4e88418c461eb5317731db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:54:40 +0000 Subject: [PATCH 2/8] Add backend authorization checks for queue operations Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/api/routers/session_queue.py | 116 ++++++++++++++--- .../session_queue/session_queue_base.py | 32 +++-- .../session_queue/session_queue_sqlite.py | 122 ++++++++++++++---- 3 files changed, 215 insertions(+), 55 deletions(-) diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py index 8ba033bb19b..8add70fe4c2 100644 --- a/invokeai/app/api/routers/session_queue.py +++ b/invokeai/app/api/routers/session_queue.py @@ -4,7 +4,7 @@ from fastapi.routing import APIRouter from pydantic import BaseModel -from invokeai.app.api.auth_dependencies import CurrentUser +from invokeai.app.api.auth_dependencies import AdminUser, CurrentUser from invokeai.app.api.dependencies import ApiDependencies from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus from invokeai.app.services.session_queue.session_queue_common import ( @@ -167,9 +167,10 @@ async def get_queue_items_by_item_ids( responses={200: {"model": SessionProcessorStatus}}, ) async def resume( + current_user: AdminUser, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> SessionProcessorStatus: - """Resumes session processor""" + """Resumes session processor. Admin only.""" try: return ApiDependencies.invoker.services.session_processor.resume() except Exception as e: @@ -182,9 +183,10 @@ async def resume( responses={200: {"model": SessionProcessorStatus}}, ) async def Pause( + current_user: AdminUser, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> SessionProcessorStatus: - """Pauses session processor""" + """Pauses session processor. Admin only.""" try: return ApiDependencies.invoker.services.session_processor.pause() except Exception as e: @@ -197,11 +199,16 @@ async def Pause( responses={200: {"model": CancelAllExceptCurrentResult}}, ) async def cancel_all_except_current( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> CancelAllExceptCurrentResult: - """Immediately cancels all queue items except in-processing items""" + """Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items.""" try: - return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id) + # Admin users can cancel all items, non-admin users can only cancel their own + user_id = None if current_user.is_admin else current_user.user_id + return ApiDependencies.invoker.services.session_queue.cancel_all_except_current( + queue_id=queue_id, user_id=user_id + ) except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while canceling all except current: {e}") @@ -212,11 +219,16 @@ async def cancel_all_except_current( responses={200: {"model": DeleteAllExceptCurrentResult}}, ) async def delete_all_except_current( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> DeleteAllExceptCurrentResult: - """Immediately deletes all queue items except in-processing items""" + """Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items.""" try: - return ApiDependencies.invoker.services.session_queue.delete_all_except_current(queue_id=queue_id) + # Admin users can delete all items, non-admin users can only delete their own + user_id = None if current_user.is_admin else current_user.user_id + return ApiDependencies.invoker.services.session_queue.delete_all_except_current( + queue_id=queue_id, user_id=user_id + ) except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while deleting all except current: {e}") @@ -227,13 +239,16 @@ async def delete_all_except_current( responses={200: {"model": CancelByBatchIDsResult}}, ) async def cancel_by_batch_ids( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), batch_ids: list[str] = Body(description="The list of batch_ids to cancel all queue items for", embed=True), ) -> CancelByBatchIDsResult: - """Immediately cancels all queue items from the given batch ids""" + """Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items.""" try: + # Admin users can cancel all items, non-admin users can only cancel their own + user_id = None if current_user.is_admin else current_user.user_id return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids( - queue_id=queue_id, batch_ids=batch_ids + queue_id=queue_id, batch_ids=batch_ids, user_id=user_id ) except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while canceling by batch id: {e}") @@ -245,13 +260,16 @@ async def cancel_by_batch_ids( responses={200: {"model": CancelByDestinationResult}}, ) async def cancel_by_destination( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), destination: str = Query(description="The destination to cancel all queue items for"), ) -> CancelByDestinationResult: - """Immediately cancels all queue items with the given origin""" + """Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items.""" try: + # Admin users can cancel all items, non-admin users can only cancel their own + user_id = None if current_user.is_admin else current_user.user_id return ApiDependencies.invoker.services.session_queue.cancel_by_destination( - queue_id=queue_id, destination=destination + queue_id=queue_id, destination=destination, user_id=user_id ) except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while canceling by destination: {e}") @@ -263,12 +281,29 @@ async def cancel_by_destination( responses={200: {"model": RetryItemsResult}}, ) async def retry_items_by_id( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), item_ids: list[int] = Body(description="The queue item ids to retry"), ) -> RetryItemsResult: - """Immediately cancels all queue items with the given origin""" + """Retries the given queue items. Users can only retry their own items unless they are an admin.""" try: + # Check authorization: user must own all items or be an admin + if not current_user.is_admin: + for item_id in item_ids: + try: + queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) + if queue_item.user_id != current_user.user_id: + raise HTTPException( + status_code=403, + detail=f"You do not have permission to retry queue item {item_id}" + ) + except SessionQueueItemNotFoundError: + # Skip items that don't exist - they will be handled by retry_items_by_id + continue + return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids) + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while retrying queue items: {e}") @@ -281,15 +316,24 @@ async def retry_items_by_id( }, ) async def clear( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> ClearResult: - """Clears the queue entirely, immediately canceling the currently-executing session""" + """Clears the queue entirely. If there's a currently-executing item, users can only cancel it if they own it or are an admin.""" try: queue_item = ApiDependencies.invoker.services.session_queue.get_current(queue_id) if queue_item is not None: + # Check authorization for canceling the current item + if queue_item.user_id != current_user.user_id and not current_user.is_admin: + raise HTTPException( + status_code=403, + detail="You do not have permission to cancel the currently executing queue item" + ) ApiDependencies.invoker.services.session_queue.cancel_queue_item(queue_item.item_id) clear_result = ApiDependencies.invoker.services.session_queue.clear(queue_id) return clear_result + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while clearing queue: {e}") @@ -302,11 +346,14 @@ async def clear( }, ) async def prune( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> PruneResult: - """Prunes all completed or errored queue items""" + """Prunes all completed or errored queue items. Non-admin users can only prune their own items.""" try: - return ApiDependencies.invoker.services.session_queue.prune(queue_id) + # Admin users can prune all items, non-admin users can only prune their own + user_id = None if current_user.is_admin else current_user.user_id + return ApiDependencies.invoker.services.session_queue.prune(queue_id, user_id=user_id) except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while pruning queue: {e}") @@ -413,12 +460,27 @@ async def get_queue_item( operation_id="delete_queue_item", ) async def delete_queue_item( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), item_id: int = Path(description="The queue item to delete"), ) -> None: - """Deletes a queue item""" + """Deletes a queue item. Users can only delete their own items unless they are an admin.""" try: + # Get the queue item to check ownership + queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) + + # Check authorization: user must own the item or be an admin + if queue_item.user_id != current_user.user_id and not current_user.is_admin: + raise HTTPException( + status_code=403, + detail="You do not have permission to delete this queue item" + ) + ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id) + except SessionQueueItemNotFoundError: + raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}") + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while deleting queue item: {e}") @@ -431,14 +493,27 @@ async def delete_queue_item( }, ) async def cancel_queue_item( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to perform this operation on"), item_id: int = Path(description="The queue item to cancel"), ) -> SessionQueueItem: - """Deletes a queue item""" + """Cancels a queue item. Users can only cancel their own items unless they are an admin.""" try: + # Get the queue item to check ownership + queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) + + # Check authorization: user must own the item or be an admin + if queue_item.user_id != current_user.user_id and not current_user.is_admin: + raise HTTPException( + status_code=403, + detail="You do not have permission to cancel this queue item" + ) + return ApiDependencies.invoker.services.session_queue.cancel_queue_item(item_id) except SessionQueueItemNotFoundError: raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}") + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while canceling queue item: {e}") @@ -467,13 +542,16 @@ async def counts_by_destination( responses={200: {"model": DeleteByDestinationResult}}, ) async def delete_by_destination( + current_user: CurrentUser, queue_id: str = Path(description="The queue id to query"), destination: str = Path(description="The destination to query"), ) -> DeleteByDestinationResult: - """Deletes all items with the given destination""" + """Deletes all items with the given destination. Non-admin users can only delete their own items.""" try: + # Admin users can delete all items, non-admin users can only delete their own + user_id = None if current_user.is_admin else current_user.user_id return ApiDependencies.invoker.services.session_queue.delete_by_destination( - queue_id=queue_id, destination=destination + queue_id=queue_id, destination=destination, user_id=user_id ) except Exception as e: raise HTTPException(status_code=500, detail=f"Unexpected error while deleting by destination: {e}") diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py index e6c24f14e77..3755b529d36 100644 --- a/invokeai/app/services/session_queue/session_queue_base.py +++ b/invokeai/app/services/session_queue/session_queue_base.py @@ -58,8 +58,8 @@ def clear(self, queue_id: str) -> ClearResult: pass @abstractmethod - def prune(self, queue_id: str) -> PruneResult: - """Deletes all completed and errored session queue items""" + def prune(self, queue_id: str, user_id: Optional[str] = None) -> PruneResult: + """Deletes all completed and errored session queue items. If user_id is provided, only prunes items owned by that user.""" pass @abstractmethod @@ -110,18 +110,24 @@ def fail_queue_item( pass @abstractmethod - def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: - """Cancels all queue items with matching batch IDs""" + def cancel_by_batch_ids( + self, queue_id: str, batch_ids: list[str], user_id: Optional[str] = None + ) -> CancelByBatchIDsResult: + """Cancels all queue items with matching batch IDs. If user_id is provided, only cancels items owned by that user.""" pass @abstractmethod - def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult: - """Cancels all queue items with the given batch destination""" + def cancel_by_destination( + self, queue_id: str, destination: str, user_id: Optional[str] = None + ) -> CancelByDestinationResult: + """Cancels all queue items with the given batch destination. If user_id is provided, only cancels items owned by that user.""" pass @abstractmethod - def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDestinationResult: - """Deletes all queue items with the given batch destination""" + def delete_by_destination( + self, queue_id: str, destination: str, user_id: Optional[str] = None + ) -> DeleteByDestinationResult: + """Deletes all queue items with the given batch destination. If user_id is provided, only deletes items owned by that user.""" pass @abstractmethod @@ -130,13 +136,15 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult: pass @abstractmethod - def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResult: - """Cancels all queue items except in-progress items""" + def cancel_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> CancelAllExceptCurrentResult: + """Cancels all queue items except in-progress items. If user_id is provided, only cancels items owned by that user.""" pass @abstractmethod - def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult: - """Deletes all queue items except in-progress items""" + def delete_all_except_current( + self, queue_id: str, user_id: Optional[str] = None + ) -> DeleteAllExceptCurrentResult: + """Deletes all queue items except in-progress items. If user_id is provided, only deletes items owned by that user.""" pass @abstractmethod diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 5c6f529bbba..0aa7f04830e 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -314,9 +314,11 @@ def clear(self, queue_id: str) -> ClearResult: self.__invoker.services.events.emit_queue_cleared(queue_id) return ClearResult(deleted=count) - def prune(self, queue_id: str) -> PruneResult: + def prune(self, queue_id: str, user_id: Optional[str] = None) -> PruneResult: with self._db.transaction() as cursor: - where = """--sql + # Build WHERE clause with optional user_id filter + user_filter = "AND user_id = ?" if user_id is not None else "" + where = f"""--sql WHERE queue_id = ? AND ( @@ -324,14 +326,19 @@ def prune(self, queue_id: str) -> PruneResult: OR status = 'failed' OR status = 'canceled' ) + {user_filter} """ + params = [queue_id] + if user_id is not None: + params.append(user_id) + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) count = cursor.fetchone()[0] cursor.execute( @@ -340,7 +347,7 @@ def prune(self, queue_id: str) -> PruneResult: FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) return PruneResult(deleted=count) @@ -384,10 +391,15 @@ def fail_queue_item( ) return queue_item - def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: + def cancel_by_batch_ids( + self, queue_id: str, batch_ids: list[str], user_id: Optional[str] = None + ) -> CancelByBatchIDsResult: with self._db.transaction() as cursor: current_queue_item = self.get_current(queue_id) placeholders = ", ".join(["?" for _ in batch_ids]) + + # Build WHERE clause with optional user_id filter + user_filter = "AND user_id = ?" if user_id is not None else "" where = f"""--sql WHERE queue_id == ? @@ -397,8 +409,12 @@ def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBa AND status != 'failed' -- We will cancel the current item separately below - skip it here AND status != 'in_progress' + {user_filter} """ params = [queue_id] + batch_ids + if user_id is not None: + params.append(user_id) + cursor.execute( f"""--sql SELECT COUNT(*) @@ -417,15 +433,22 @@ def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBa tuple(params), ) + # Handle current item separately - check ownership if user_id is provided if current_queue_item is not None and current_queue_item.batch_id in batch_ids: - self._set_queue_item_status(current_queue_item.item_id, "canceled") + if user_id is None or current_queue_item.user_id == user_id: + self._set_queue_item_status(current_queue_item.item_id, "canceled") return CancelByBatchIDsResult(canceled=count) - def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult: + def cancel_by_destination( + self, queue_id: str, destination: str, user_id: Optional[str] = None + ) -> CancelByDestinationResult: with self._db.transaction() as cursor: current_queue_item = self.get_current(queue_id) - where = """--sql + + # Build WHERE clause with optional user_id filter + user_filter = "AND user_id = ?" if user_id is not None else "" + where = f"""--sql WHERE queue_id == ? AND destination == ? @@ -434,15 +457,19 @@ def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDest AND status != 'failed' -- We will cancel the current item separately below - skip it here AND status != 'in_progress' + {user_filter} """ - params = (queue_id, destination) + params = [queue_id, destination] + if user_id is not None: + params.append(user_id) + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue {where}; """, - params, + tuple(params), ) count = cursor.fetchone()[0] cursor.execute( @@ -451,23 +478,56 @@ def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDest SET status = 'canceled' {where}; """, - params, + tuple(params), ) + + # Handle current item separately - check ownership if user_id is provided if current_queue_item is not None and current_queue_item.destination == destination: - self._set_queue_item_status(current_queue_item.item_id, "canceled") + if user_id is None or current_queue_item.user_id == user_id: + self._set_queue_item_status(current_queue_item.item_id, "canceled") + return CancelByDestinationResult(canceled=count) - def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDestinationResult: + def delete_by_destination( + self, queue_id: str, destination: str, user_id: Optional[str] = None + ) -> DeleteByDestinationResult: with self._db.transaction() as cursor: current_queue_item = self.get_current(queue_id) + + # Handle current item separately - check ownership if user_id is provided if current_queue_item is not None and current_queue_item.destination == destination: - self.cancel_queue_item(current_queue_item.item_id) - params = (queue_id, destination) + if user_id is None or current_queue_item.user_id == user_id: + self.cancel_queue_item(current_queue_item.item_id) + + # Build WHERE clause with optional user_id filter + user_filter = "AND user_id = ?" if user_id is not None else "" + params = [queue_id, destination] + if user_id is not None: + params.append(user_id) + cursor.execute( - """--sql + f"""--sql SELECT COUNT(*) FROM session_queue WHERE + queue_id == ? + AND destination == ? + {user_filter} + """, + tuple(params), + ) + count = cursor.fetchone()[0] + cursor.execute( + f"""--sql + DELETE FROM session_queue + WHERE + queue_id == ? + AND destination == ? + {user_filter} + """, + tuple(params), + ) + return DeleteByDestinationResult(deleted=count) queue_id = ? AND destination = ?; """, @@ -486,20 +546,27 @@ def delete_by_destination(self, queue_id: str, destination: str) -> DeleteByDest ) return DeleteByDestinationResult(deleted=count) - def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult: + def delete_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> DeleteAllExceptCurrentResult: with self._db.transaction() as cursor: - where = """--sql + # Build WHERE clause with optional user_id filter + user_filter = "AND user_id = ?" if user_id is not None else "" + where = f"""--sql WHERE queue_id == ? AND status == 'pending' + {user_filter} """ + params = [queue_id] + if user_id is not None: + params.append(user_id) + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) count = cursor.fetchone()[0] cursor.execute( @@ -508,7 +575,7 @@ def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResu FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) return DeleteAllExceptCurrentResult(deleted=count) @@ -547,20 +614,27 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult: self._set_queue_item_status(current_queue_item.item_id, "canceled") return CancelByQueueIDResult(canceled=count) - def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResult: + def cancel_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> CancelAllExceptCurrentResult: with self._db.transaction() as cursor: - where = """--sql + # Build WHERE clause with optional user_id filter + user_filter = "AND user_id = ?" if user_id is not None else "" + where = f"""--sql WHERE queue_id == ? AND status == 'pending' + {user_filter} """ + params = [queue_id] + if user_id is not None: + params.append(user_id) + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) count = cursor.fetchone()[0] cursor.execute( @@ -569,7 +643,7 @@ def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResu SET status = 'canceled' {where}; """, - (queue_id,), + tuple(params), ) return CancelAllExceptCurrentResult(canceled=count) From 0f47eac776591c147c92db4fb28c73f8fd501e18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:56:07 +0000 Subject: [PATCH 3/8] Fix linting issues in authorization changes Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/api/routers/session_queue.py | 26 ++++-------- .../session_queue/session_queue_base.py | 4 +- .../session_queue/session_queue_sqlite.py | 41 ++++++------------- 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py index 8add70fe4c2..e6c2b311902 100644 --- a/invokeai/app/api/routers/session_queue.py +++ b/invokeai/app/api/routers/session_queue.py @@ -294,13 +294,12 @@ async def retry_items_by_id( queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) if queue_item.user_id != current_user.user_id: raise HTTPException( - status_code=403, - detail=f"You do not have permission to retry queue item {item_id}" + status_code=403, detail=f"You do not have permission to retry queue item {item_id}" ) except SessionQueueItemNotFoundError: # Skip items that don't exist - they will be handled by retry_items_by_id continue - + return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids) except HTTPException: raise @@ -326,8 +325,7 @@ async def clear( # Check authorization for canceling the current item if queue_item.user_id != current_user.user_id and not current_user.is_admin: raise HTTPException( - status_code=403, - detail="You do not have permission to cancel the currently executing queue item" + status_code=403, detail="You do not have permission to cancel the currently executing queue item" ) ApiDependencies.invoker.services.session_queue.cancel_queue_item(queue_item.item_id) clear_result = ApiDependencies.invoker.services.session_queue.clear(queue_id) @@ -468,14 +466,11 @@ async def delete_queue_item( try: # Get the queue item to check ownership queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) - + # Check authorization: user must own the item or be an admin if queue_item.user_id != current_user.user_id and not current_user.is_admin: - raise HTTPException( - status_code=403, - detail="You do not have permission to delete this queue item" - ) - + raise HTTPException(status_code=403, detail="You do not have permission to delete this queue item") + ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id) except SessionQueueItemNotFoundError: raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}") @@ -501,14 +496,11 @@ async def cancel_queue_item( try: # Get the queue item to check ownership queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) - + # Check authorization: user must own the item or be an admin if queue_item.user_id != current_user.user_id and not current_user.is_admin: - raise HTTPException( - status_code=403, - detail="You do not have permission to cancel this queue item" - ) - + raise HTTPException(status_code=403, detail="You do not have permission to cancel this queue item") + return ApiDependencies.invoker.services.session_queue.cancel_queue_item(item_id) except SessionQueueItemNotFoundError: raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}") diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py index 3755b529d36..5232dc9c76e 100644 --- a/invokeai/app/services/session_queue/session_queue_base.py +++ b/invokeai/app/services/session_queue/session_queue_base.py @@ -141,9 +141,7 @@ def cancel_all_except_current(self, queue_id: str, user_id: Optional[str] = None pass @abstractmethod - def delete_all_except_current( - self, queue_id: str, user_id: Optional[str] = None - ) -> DeleteAllExceptCurrentResult: + def delete_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> DeleteAllExceptCurrentResult: """Deletes all queue items except in-progress items. If user_id is provided, only deletes items owned by that user.""" pass diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 0aa7f04830e..aa5ce689b40 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -331,7 +331,7 @@ def prune(self, queue_id: str, user_id: Optional[str] = None) -> PruneResult: params = [queue_id] if user_id is not None: params.append(user_id) - + cursor.execute( f"""--sql SELECT COUNT(*) @@ -397,7 +397,7 @@ def cancel_by_batch_ids( with self._db.transaction() as cursor: current_queue_item = self.get_current(queue_id) placeholders = ", ".join(["?" for _ in batch_ids]) - + # Build WHERE clause with optional user_id filter user_filter = "AND user_id = ?" if user_id is not None else "" where = f"""--sql @@ -414,7 +414,7 @@ def cancel_by_batch_ids( params = [queue_id] + batch_ids if user_id is not None: params.append(user_id) - + cursor.execute( f"""--sql SELECT COUNT(*) @@ -445,7 +445,7 @@ def cancel_by_destination( ) -> CancelByDestinationResult: with self._db.transaction() as cursor: current_queue_item = self.get_current(queue_id) - + # Build WHERE clause with optional user_id filter user_filter = "AND user_id = ?" if user_id is not None else "" where = f"""--sql @@ -462,7 +462,7 @@ def cancel_by_destination( params = [queue_id, destination] if user_id is not None: params.append(user_id) - + cursor.execute( f"""--sql SELECT COUNT(*) @@ -480,12 +480,12 @@ def cancel_by_destination( """, tuple(params), ) - + # Handle current item separately - check ownership if user_id is provided if current_queue_item is not None and current_queue_item.destination == destination: if user_id is None or current_queue_item.user_id == user_id: self._set_queue_item_status(current_queue_item.item_id, "canceled") - + return CancelByDestinationResult(canceled=count) def delete_by_destination( @@ -493,18 +493,18 @@ def delete_by_destination( ) -> DeleteByDestinationResult: with self._db.transaction() as cursor: current_queue_item = self.get_current(queue_id) - + # Handle current item separately - check ownership if user_id is provided if current_queue_item is not None and current_queue_item.destination == destination: if user_id is None or current_queue_item.user_id == user_id: self.cancel_queue_item(current_queue_item.item_id) - + # Build WHERE clause with optional user_id filter user_filter = "AND user_id = ?" if user_id is not None else "" params = [queue_id, destination] if user_id is not None: params.append(user_id) - + cursor.execute( f"""--sql SELECT COUNT(*) @@ -527,23 +527,6 @@ def delete_by_destination( """, tuple(params), ) - return DeleteByDestinationResult(deleted=count) - queue_id = ? - AND destination = ?; - """, - params, - ) - count = cursor.fetchone()[0] - cursor.execute( - """--sql - DELETE - FROM session_queue - WHERE - queue_id = ? - AND destination = ?; - """, - params, - ) return DeleteByDestinationResult(deleted=count) def delete_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> DeleteAllExceptCurrentResult: @@ -559,7 +542,7 @@ def delete_all_except_current(self, queue_id: str, user_id: Optional[str] = None params = [queue_id] if user_id is not None: params.append(user_id) - + cursor.execute( f"""--sql SELECT COUNT(*) @@ -627,7 +610,7 @@ def cancel_all_except_current(self, queue_id: str, user_id: Optional[str] = None params = [queue_id] if user_id is not None: params.append(user_id) - + cursor.execute( f"""--sql SELECT COUNT(*) From 87a59cc7acc231e4a8e389be6307a808b1fb9c31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:00:04 +0000 Subject: [PATCH 4/8] Add frontend authorization checks for queue operations Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- .../QueueList/QueueItemComponent.tsx | 16 +++++++++++++++- .../queue/hooks/useCancelCurrentQueueItem.ts | 19 +++++++++++++++++-- .../features/queue/hooks/usePauseProcessor.ts | 10 ++++++++-- .../queue/hooks/useResumeProcessor.ts | 10 ++++++++-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx index 8f673651bad..511c9c855ea 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx @@ -1,5 +1,7 @@ import type { ChakraProps, CollapseProps, FlexProps } from '@invoke-ai/ui-library'; import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge'; import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText'; import { useOriginText } from 'features/queue/components/QueueList/useOriginText'; @@ -31,6 +33,17 @@ const QueueItemComponent = ({ index, item }: InnerItemProps) => { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const handleToggle = useCallback(() => setIsOpen((s) => !s), [setIsOpen]); + const currentUser = useAppSelector(selectCurrentUser); + + // Check if current user can manage this queue item + const canManageItem = useMemo(() => { + if (!currentUser) return false; + // Admin users can manage all items + if (currentUser.is_admin) return true; + // Non-admin users can only manage their own items + return item.user_id === currentUser.user_id; + }, [currentUser, item.user_id]); + const cancelQueueItem = useCancelQueueItem(); const onClickCancelQueueItem = useCallback( (e: MouseEvent) => { @@ -138,7 +151,7 @@ const QueueItemComponent = ({ index, item }: InnerItemProps) => { {!isFailed && ( } @@ -147,6 +160,7 @@ const QueueItemComponent = ({ index, item }: InnerItemProps) => { {isFailed && ( } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts index 98213288710..78d26d592fd 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts @@ -1,11 +1,26 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useGetCurrentQueueItemQuery } from 'services/api/endpoints/queue'; import { useCancelQueueItem } from './useCancelQueueItem'; export const useCancelCurrentQueueItem = () => { const currentQueueItemId = useCurrentQueueItemId(); + const { data: currentQueueItem } = useGetCurrentQueueItemQuery(); + const currentUser = useAppSelector(selectCurrentUser); const cancelQueueItem = useCancelQueueItem(); + + // Check if current user can cancel the current item + const canCancelCurrentItem = useMemo(() => { + if (!currentUser || !currentQueueItem) return false; + // Admin users can cancel all items + if (currentUser.is_admin) return true; + // Non-admin users can only cancel their own items + return currentQueueItem.user_id === currentUser.user_id; + }, [currentUser, currentQueueItem]); + const trigger = useCallback( (options?: { withToast?: boolean }) => { if (currentQueueItemId === null) { @@ -19,6 +34,6 @@ export const useCancelCurrentQueueItem = () => { return { trigger, isLoading: cancelQueueItem.isLoading, - isDisabled: cancelQueueItem.isDisabled || currentQueueItemId === null, + isDisabled: cancelQueueItem.isDisabled || currentQueueItemId === null || !canCancelCurrentItem, }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts index 9e82576a4f4..591650db938 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts @@ -1,6 +1,8 @@ import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; import { toast } from 'features/toast/toast'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery, usePauseProcessorMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -9,9 +11,13 @@ export const usePauseProcessor = () => { const { t } = useTranslation(); const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); + const currentUser = useAppSelector(selectCurrentUser); const [_trigger, { isLoading }] = usePauseProcessorMutation({ fixedCacheKey: 'pauseProcessor', }); + + // Only admin users can pause the processor + const isAdmin = useMemo(() => currentUser?.is_admin ?? false, [currentUser]); const trigger = useCallback(async () => { try { @@ -30,5 +36,5 @@ export const usePauseProcessor = () => { } }, [_trigger, t]); - return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started }; + return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started || !isAdmin }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts index 901bac39f83..909109b9541 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts @@ -1,6 +1,8 @@ import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; import { toast } from 'features/toast/toast'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery, useResumeProcessorMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -9,9 +11,13 @@ export const useResumeProcessor = () => { const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); const { t } = useTranslation(); + const currentUser = useAppSelector(selectCurrentUser); const [_trigger, { isLoading }] = useResumeProcessorMutation({ fixedCacheKey: 'resumeProcessor', }); + + // Only admin users can resume the processor + const isAdmin = useMemo(() => currentUser?.is_admin ?? false, [currentUser]); const trigger = useCallback(async () => { try { @@ -30,5 +36,5 @@ export const useResumeProcessor = () => { } }, [_trigger, t]); - return { trigger, isLoading, isDisabled: !isConnected || queueStatus?.processor.is_started }; + return { trigger, isLoading, isDisabled: !isConnected || queueStatus?.processor.is_started || !isAdmin }; }; From b16b5adc47e89cee84d7d9d5b2a965a499a73deb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:52:06 +0000 Subject: [PATCH 5/8] Add access denied messages for cancel and clear operations Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/frontend/web/public/locales/en.json | 2 ++ .../web/src/features/queue/hooks/useCancelQueueItem.ts | 6 ++++-- .../frontend/web/src/features/queue/hooks/useClearQueue.ts | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 89a45db79d4..e5a5e55c27e 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -304,6 +304,7 @@ "cancelTooltip": "Cancel Current Item", "cancelSucceeded": "Item Canceled", "cancelFailed": "Problem Canceling Item", + "cancelFailedAccessDenied": "Problem Canceling Item: Access Denied", "retrySucceeded": "Item Retried", "retryFailed": "Problem Retrying Item", "confirm": "Confirm", @@ -315,6 +316,7 @@ "clearTooltip": "Cancel and Clear All Items", "clearSucceeded": "Queue Cleared", "clearFailed": "Problem Clearing Queue", + "clearFailedAccessDenied": "Problem Clearing Queue: Access Denied", "cancelBatch": "Cancel Batch", "cancelItem": "Cancel Item", "retryItem": "Retry Item", diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts index c122241cbd1..b85fe8d3734 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts @@ -25,11 +25,13 @@ export const useCancelQueueItem = () => { status: 'success', }); } - } catch { + } catch (error) { if (withToast) { + // Check if this is a 403 access denied error + const isAccessDenied = error instanceof Object && 'status' in error && error.status === 403; toast({ id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), + title: isAccessDenied ? t('queue.cancelFailedAccessDenied') : t('queue.cancelFailed'), status: 'error', }); } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts index a81f7254be3..bd6ea2cc02d 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts @@ -25,10 +25,12 @@ export const useClearQueue = () => { title: t('queue.clearSucceeded'), status: 'success', }); - } catch { + } catch (error) { + // Check if this is a 403 access denied error + const isAccessDenied = error instanceof Object && 'status' in error && error.status === 403; toast({ id: 'QUEUE_CLEAR_FAILED', - title: t('queue.clearFailed'), + title: isAccessDenied ? t('queue.clearFailedAccessDenied') : t('queue.clearFailed'), status: 'error', }); } From 7eff0eeb42524ee59c6f2895e1d3a4a393b8fac2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:03:12 +0000 Subject: [PATCH 6/8] Fix access denied messages for all cancel/delete operations Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- .../queue/hooks/useCancelAllExceptCurrentQueueItem.ts | 6 ++++-- .../queue/hooks/useCancelQueueItemsByDestination.ts | 6 ++++-- .../queue/hooks/useDeleteAllExceptCurrentQueueItem.ts | 6 ++++-- .../web/src/features/queue/hooks/useDeleteQueueItem.ts | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts index 8e6c79b96a0..d36ed0fc589 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts @@ -25,10 +25,12 @@ export const useCancelAllExceptCurrentQueueItem = () => { title: t('queue.cancelSucceeded'), status: 'success', }); - } catch { + } catch (error) { + // Check if this is a 403 access denied error + const isAccessDenied = error instanceof Object && 'status' in error && error.status === 403; toast({ id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), + title: isAccessDenied ? t('queue.cancelFailedAccessDenied') : t('queue.cancelFailed'), status: 'error', }); } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts index 14864e0e3f5..df0eabcb527 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts @@ -26,11 +26,13 @@ export const useCancelQueueItemsByDestination = () => { status: 'success', }); } - } catch { + } catch (error) { if (withToast) { + // Check if this is a 403 access denied error + const isAccessDenied = error instanceof Object && 'status' in error && error.status === 403; toast({ id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), + title: isAccessDenied ? t('queue.cancelFailedAccessDenied') : t('queue.cancelFailed'), status: 'error', }); } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts index 1f34a76d24d..b96c3914703 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts @@ -25,10 +25,12 @@ export const useDeleteAllExceptCurrentQueueItem = () => { title: t('queue.cancelSucceeded'), status: 'success', }); - } catch { + } catch (error) { + // Check if this is a 403 access denied error + const isAccessDenied = error instanceof Object && 'status' in error && error.status === 403; toast({ id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), + title: isAccessDenied ? t('queue.cancelFailedAccessDenied') : t('queue.cancelFailed'), status: 'error', }); } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts index af91196ddfe..699a81ac740 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts @@ -25,11 +25,13 @@ export const useDeleteQueueItem = () => { status: 'success', }); } - } catch { + } catch (error) { if (withToast) { + // Check if this is a 403 access denied error + const isAccessDenied = error instanceof Object && 'status' in error && error.status === 403; toast({ id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), + title: isAccessDenied ? t('queue.cancelFailedAccessDenied') : t('queue.cancelFailed'), status: 'error', }); } From d21b819ba35a0c17a7fb59a64ee864a56c092109 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:26:32 +0000 Subject: [PATCH 7/8] Fix merge conflict duplicates in QueueItemComponent Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/frontend/web/public/locales/en.json | 2 +- .../components/QueueList/QueueItemComponent.tsx | 13 +++++++------ .../queue/hooks/useCancelCurrentQueueItem.ts | 12 ++++++++---- .../src/features/queue/hooks/usePauseProcessor.ts | 2 +- .../src/features/queue/hooks/useResumeProcessor.ts | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 1262b0094f1..4f1f4831820 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -347,7 +347,7 @@ "other": "Other", "gallery": "Gallery", "batchFieldValues": "Batch Field Values", - "fieldValuesHidden": "Hidden for privacy", + "fieldValuesHidden": "", "cannotViewDetails": "You do not have permission to view the details of this queue item", "item": "Item", "session": "Session", diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx index bd8c5245fe0..15ededc99c5 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx @@ -32,19 +32,20 @@ const sx: ChakraProps['sx'] = { const QueueItemComponent = ({ index, item }: InnerItemProps) => { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); - const handleToggle = useCallback(() => setIsOpen((s) => !s), [setIsOpen]); const currentUser = useAppSelector(selectCurrentUser); - + // Check if current user can manage this queue item const canManageItem = useMemo(() => { - if (!currentUser) return false; + if (!currentUser) { + return false; + } // Admin users can manage all items - if (currentUser.is_admin) return true; + if (currentUser.is_admin) { + return true; + } // Non-admin users can only manage their own items return item.user_id === currentUser.user_id; }, [currentUser, item.user_id]); - - const currentUser = useAppSelector(selectCurrentUser); // Check if the current user can view this queue item's details const canViewDetails = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts index 78d26d592fd..797a940507b 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts @@ -11,16 +11,20 @@ export const useCancelCurrentQueueItem = () => { const { data: currentQueueItem } = useGetCurrentQueueItemQuery(); const currentUser = useAppSelector(selectCurrentUser); const cancelQueueItem = useCancelQueueItem(); - + // Check if current user can cancel the current item const canCancelCurrentItem = useMemo(() => { - if (!currentUser || !currentQueueItem) return false; + if (!currentUser || !currentQueueItem) { + return false; + } // Admin users can cancel all items - if (currentUser.is_admin) return true; + if (currentUser.is_admin) { + return true; + } // Non-admin users can only cancel their own items return currentQueueItem.user_id === currentUser.user_id; }, [currentUser, currentQueueItem]); - + const trigger = useCallback( (options?: { withToast?: boolean }) => { if (currentQueueItemId === null) { diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts index 591650db938..bc0a95d7bb2 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts @@ -15,7 +15,7 @@ export const usePauseProcessor = () => { const [_trigger, { isLoading }] = usePauseProcessorMutation({ fixedCacheKey: 'pauseProcessor', }); - + // Only admin users can pause the processor const isAdmin = useMemo(() => currentUser?.is_admin ?? false, [currentUser]); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts index 909109b9541..10961abde0c 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts @@ -15,7 +15,7 @@ export const useResumeProcessor = () => { const [_trigger, { isLoading }] = useResumeProcessorMutation({ fixedCacheKey: 'resumeProcessor', }); - + // Only admin users can resume the processor const isAdmin = useMemo(() => currentUser?.is_admin ?? false, [currentUser]); From 834689dbca61ea769002d9f018a7d854a71fca2a Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 19 Jan 2026 21:49:02 -0500 Subject: [PATCH 8/8] chore(frontend): typegen --- .../frontend/web/src/services/api/schema.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 6562358551e..24323c92dc5 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1444,7 +1444,7 @@ export type paths = { get?: never; /** * Resume - * @description Resumes session processor + * @description Resumes session processor. Admin only. */ put: operations["resume"]; post?: never; @@ -1464,7 +1464,7 @@ export type paths = { get?: never; /** * Pause - * @description Pauses session processor + * @description Pauses session processor. Admin only. */ put: operations["pause"]; post?: never; @@ -1484,7 +1484,7 @@ export type paths = { get?: never; /** * Cancel All Except Current - * @description Immediately cancels all queue items except in-processing items + * @description Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items. */ put: operations["cancel_all_except_current"]; post?: never; @@ -1504,7 +1504,7 @@ export type paths = { get?: never; /** * Delete All Except Current - * @description Immediately deletes all queue items except in-processing items + * @description Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items. */ put: operations["delete_all_except_current"]; post?: never; @@ -1524,7 +1524,7 @@ export type paths = { get?: never; /** * Cancel By Batch Ids - * @description Immediately cancels all queue items from the given batch ids + * @description Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items. */ put: operations["cancel_by_batch_ids"]; post?: never; @@ -1544,7 +1544,7 @@ export type paths = { get?: never; /** * Cancel By Destination - * @description Immediately cancels all queue items with the given origin + * @description Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items. */ put: operations["cancel_by_destination"]; post?: never; @@ -1564,7 +1564,7 @@ export type paths = { get?: never; /** * Retry Items By Id - * @description Immediately cancels all queue items with the given origin + * @description Retries the given queue items. Users can only retry their own items unless they are an admin. */ put: operations["retry_items_by_id"]; post?: never; @@ -1584,7 +1584,7 @@ export type paths = { get?: never; /** * Clear - * @description Clears the queue entirely, immediately canceling the currently-executing session + * @description Clears the queue entirely. If there's a currently-executing item, users can only cancel it if they own it or are an admin. */ put: operations["clear"]; post?: never; @@ -1604,7 +1604,7 @@ export type paths = { get?: never; /** * Prune - * @description Prunes all completed or errored queue items + * @description Prunes all completed or errored queue items. Non-admin users can only prune their own items. */ put: operations["prune"]; post?: never; @@ -1710,7 +1710,7 @@ export type paths = { post?: never; /** * Delete Queue Item - * @description Deletes a queue item + * @description Deletes a queue item. Users can only delete their own items unless they are an admin. */ delete: operations["delete_queue_item"]; options?: never; @@ -1728,7 +1728,7 @@ export type paths = { get?: never; /** * Cancel Queue Item - * @description Deletes a queue item + * @description Cancels a queue item. Users can only cancel their own items unless they are an admin. */ put: operations["cancel_queue_item"]; post?: never; @@ -1770,7 +1770,7 @@ export type paths = { post?: never; /** * Delete By Destination - * @description Deletes all items with the given destination + * @description Deletes all items with the given destination. Non-admin users can only delete their own items. */ delete: operations["delete_by_destination"]; options?: never;