Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
66ba32b
feat: Per-user workflow libraries in multiuser mode (#114)
Copilot Mar 6, 2026
c9f2e2d
Restrict model sync to admin users only (#118)
Copilot Mar 6, 2026
1ed9349
feat: distinct splash screens for admin/non-admin users in multiuser …
Copilot Mar 6, 2026
a4bbccd
Merge branch 'lstein/feature/multiuser-splash-screens' into lstein/fe…
lstein Mar 6, 2026
d965d60
Merge branch 'main' into lstein/feature/workflow-isolation-in-multius…
lstein Mar 6, 2026
b37bc8d
Disable Save when editing another user's shared workflow in multiuser…
Copilot Mar 6, 2026
298ce54
Merge branch 'main' into lstein/feature/workflow-isolation-in-multius…
lstein Mar 9, 2026
3dbc290
chore(app): ruff
lstein Mar 9, 2026
13faa0f
Add board visibility (private/shared/public) feature with tests and UI
Copilot Mar 9, 2026
f38d1ab
Enforce read-only access for non-owners of shared/public boards in UI
Copilot Mar 9, 2026
9f8f7a1
Fix remaining board access enforcement: invoke icon, drag-out, change…
Copilot Mar 10, 2026
74c293e
chore: merge and resolve conflicts
lstein Apr 4, 2026
3559a10
chore: merge with upstream
lstein Apr 4, 2026
f128121
fix: allow drag from shared boards to non-board targets (viewer, ref …
lstein Apr 4, 2026
b4276fd
Merge branch 'lstein/feature/workflow-isolation-in-multiuser-mode' in…
lstein Apr 4, 2026
23ab8f5
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 5, 2026
ac4ef09
fix(security): add auth requirement to all sensitive routes in multim…
lstein Apr 6, 2026
24d0d38
Merge branch 'main' into copilot/enhancement-allow-shared-boards
lstein Apr 6, 2026
5ba03e9
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 7, 2026
915239b
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 7, 2026
61c884c
Merge remote-tracking branch 'refs/remotes/origin/copilot/enhancement…
lstein Apr 7, 2026
ac1f1a5
chore(backend): ruff
lstein Apr 7, 2026
ed45bd4
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 8, 2026
edd1258
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 9, 2026
b86e289
fix (backend): improve user isolation for session queue and recall pa…
lstein Apr 10, 2026
797638b
fix(workflow): do not filter default workflows in multiuser mode
lstein Apr 10, 2026
8f792fc
Merge branch 'main' into copilot/enhancement-allow-shared-boards
lstein Apr 10, 2026
c7eeb26
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions invokeai/app/api/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class SetupStatusResponse(BaseModel):
setup_required: bool = Field(description="Whether initial setup is required")
multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
strict_password_checking: bool = Field(description="Whether strict password requirements are enforced")
admin_email: str | None = Field(default=None, description="Email of the first active admin user, if any")


@auth_router.get("/status", response_model=SetupStatusResponse)
Expand All @@ -94,15 +95,25 @@ async def get_setup_status() -> SetupStatusResponse:
# If multiuser is disabled, setup is never required
if not config.multiuser:
return SetupStatusResponse(
setup_required=False, multiuser_enabled=False, strict_password_checking=config.strict_password_checking
setup_required=False,
multiuser_enabled=False,
strict_password_checking=config.strict_password_checking,
admin_email=None,
)

# In multiuser mode, check if an admin exists
user_service = ApiDependencies.invoker.services.users
setup_required = not user_service.has_admin()

# Only expose admin_email during initial setup to avoid leaking
# administrator identity on public deployments.
admin_email = user_service.get_admin_email() if setup_required else None

return SetupStatusResponse(
setup_required=setup_required, multiuser_enabled=True, strict_password_checking=config.strict_password_checking
setup_required=setup_required,
multiuser_enabled=True,
strict_password_checking=config.strict_password_checking,
admin_email=admin_email,
)


Expand Down
29 changes: 28 additions & 1 deletion invokeai/app/api/routers/board_images.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from fastapi import Body, HTTPException
from fastapi.routing import APIRouter

from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.images.images_common import AddImagesToBoardResult, RemoveImagesFromBoardResult

board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])


def _assert_board_write_access(board_id: str, current_user: CurrentUserOrDefault) -> None:
"""Raise 403 if the current user may not mutate the given board."""
try:
board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
except Exception:
raise HTTPException(status_code=404, detail="Board not found")
if not current_user.is_admin and board.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to modify this board")


@board_images_router.post(
"/",
operation_id="add_image_to_board",
Expand All @@ -17,10 +28,12 @@
response_model=AddImagesToBoardResult,
)
async def add_image_to_board(
current_user: CurrentUserOrDefault,
board_id: str = Body(description="The id of the board to add to"),
image_name: str = Body(description="The name of the image to add"),
) -> AddImagesToBoardResult:
"""Creates a board_image"""
_assert_board_write_access(board_id, current_user)
try:
added_images: set[str] = set()
affected_boards: set[str] = set()
Expand Down Expand Up @@ -48,13 +61,16 @@ async def add_image_to_board(
response_model=RemoveImagesFromBoardResult,
)
async def remove_image_from_board(
current_user: CurrentUserOrDefault,
image_name: str = Body(description="The name of the image to remove", embed=True),
) -> RemoveImagesFromBoardResult:
"""Removes an image from its board, if it had one"""
try:
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
if old_board_id != "none":
_assert_board_write_access(old_board_id, current_user)
removed_images: set[str] = set()
affected_boards: set[str] = set()
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
removed_images.add(image_name)
affected_boards.add("none")
Expand All @@ -64,6 +80,8 @@ async def remove_image_from_board(
affected_boards=list(affected_boards),
)

except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="Failed to remove image from board")

Expand All @@ -78,10 +96,12 @@ async def remove_image_from_board(
response_model=AddImagesToBoardResult,
)
async def add_images_to_board(
current_user: CurrentUserOrDefault,
board_id: str = Body(description="The id of the board to add to"),
image_names: list[str] = Body(description="The names of the images to add", embed=True),
) -> AddImagesToBoardResult:
"""Adds a list of images to a board"""
_assert_board_write_access(board_id, current_user)
try:
added_images: set[str] = set()
affected_boards: set[str] = set()
Expand Down Expand Up @@ -116,6 +136,7 @@ async def add_images_to_board(
response_model=RemoveImagesFromBoardResult,
)
async def remove_images_from_board(
current_user: CurrentUserOrDefault,
image_names: list[str] = Body(description="The names of the images to remove", embed=True),
) -> RemoveImagesFromBoardResult:
"""Removes a list of images from their board, if they had one"""
Expand All @@ -125,15 +146,21 @@ async def remove_images_from_board(
for image_name in image_names:
try:
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
if old_board_id != "none":
_assert_board_write_access(old_board_id, current_user)
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
removed_images.add(image_name)
affected_boards.add("none")
affected_boards.add(old_board_id)
except HTTPException:
raise
except Exception:
pass
return RemoveImagesFromBoardResult(
removed_images=list(removed_images),
affected_boards=list(affected_boards),
)
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=500, detail="Failed to remove images from board")
17 changes: 14 additions & 3 deletions invokeai/app/api/routers/boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy, BoardVisibility
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
Expand Down Expand Up @@ -56,7 +56,14 @@ async def get_board(
except Exception:
raise HTTPException(status_code=404, detail="Board not found")

if not current_user.is_admin and result.user_id != current_user.user_id:
# Admins can access any board.
# Owners can access their own boards.
# Shared and public boards are visible to all authenticated users.
if (
not current_user.is_admin
and result.user_id != current_user.user_id
and result.board_visibility == BoardVisibility.Private
):
raise HTTPException(status_code=403, detail="Not authorized to access this board")

return result
Expand Down Expand Up @@ -188,7 +195,11 @@ async def list_all_board_image_names(
except Exception:
raise HTTPException(status_code=404, detail="Board not found")

if not current_user.is_admin and board.user_id != current_user.user_id:
if (
not current_user.is_admin
and board.user_id != current_user.user_id
and board.board_visibility == BoardVisibility.Private
):
raise HTTPException(status_code=403, detail="Not authorized to access this board")

image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
Expand Down
Loading
Loading