feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility (#20488)
* feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility Add a new admin panel toggle (Admin > Settings > General) called "User Status" that allows administrators to globally enable or disable user status functionality. When disabled: - User status API endpoints return 403 Forbidden - Status emoji, message, and "Update your status" button are hidden from the user menu The setting: - Defaults to True (enabled) - Can be overridden via ENABLE_USER_STATUS environment variable - Persists across restarts using PersistentConfig Files modified: - backend/open_webui/config.py - Added ENABLE_USER_STATUS PersistentConfig - backend/open_webui/main.py - App state init and features dict - backend/open_webui/routers/auths.py - AdminConfig model and endpoints - backend/open_webui/routers/users.py - 403 guards on status endpoints - src/lib/components/admin/Settings/General.svelte - Toggle UI - src/lib/components/layout/Sidebar/UserMenu.svelte - Conditional status display * Update UserMenu.svelte feat: add ENABLE_USER_STATUS toggle for admin-controlled user status visibility Add a new admin panel toggle (Admin > Settings > General) called "User Status" that allows administrators to globally enable or disable user status functionality. When disabled: - User status API endpoints return 403 Forbidden - Active/Away indicator with blinking dot is hidden from the user menu - Status emoji, message, and "Update your status" button are hidden from the user menu The setting: - Defaults to True (enabled) - Can be overridden via ENABLE_USER_STATUS environment variable - Persists across restarts using PersistentConfig Files modified: - backend/open_webui/config.py - Added ENABLE_USER_STATUS PersistentConfig - backend/open_webui/main.py - App state init and features dict - backend/open_webui/routers/auths.py - AdminConfig model and endpoints - backend/open_webui/routers/users.py - 403 guards on status endpoints - src/lib/components/admin/Settings/General.svelte - Toggle UI - src/lib/components/layout/Sidebar/UserMenu.svelte - Conditional status display * nuke the indicator * fix
This commit is contained in:
@@ -1574,6 +1574,12 @@ ENABLE_NOTES = PersistentConfig(
|
|||||||
os.environ.get("ENABLE_NOTES", "True").lower() == "true",
|
os.environ.get("ENABLE_NOTES", "True").lower() == "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENABLE_USER_STATUS = PersistentConfig(
|
||||||
|
"ENABLE_USER_STATUS",
|
||||||
|
"users.enable_status",
|
||||||
|
os.environ.get("ENABLE_USER_STATUS", "True").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
|
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
|
||||||
"ENABLE_EVALUATION_ARENA_MODELS",
|
"ENABLE_EVALUATION_ARENA_MODELS",
|
||||||
"evaluation.arena.enable",
|
"evaluation.arena.enable",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ from open_webui.config import (
|
|||||||
FOLDER_MAX_FILE_COUNT,
|
FOLDER_MAX_FILE_COUNT,
|
||||||
ENABLE_CHANNELS,
|
ENABLE_CHANNELS,
|
||||||
ENABLE_NOTES,
|
ENABLE_NOTES,
|
||||||
|
ENABLE_USER_STATUS,
|
||||||
ENABLE_COMMUNITY_SHARING,
|
ENABLE_COMMUNITY_SHARING,
|
||||||
ENABLE_MESSAGE_RATING,
|
ENABLE_MESSAGE_RATING,
|
||||||
ENABLE_USER_WEBHOOKS,
|
ENABLE_USER_WEBHOOKS,
|
||||||
@@ -789,6 +790,7 @@ app.state.config.ENABLE_NOTES = ENABLE_NOTES
|
|||||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||||
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
||||||
app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
|
app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
|
||||||
|
app.state.config.ENABLE_USER_STATUS = ENABLE_USER_STATUS
|
||||||
|
|
||||||
app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
|
app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
|
||||||
app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
|
app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
|
||||||
@@ -1933,6 +1935,7 @@ async def get_app_config(request: Request):
|
|||||||
"enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
|
"enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||||
"enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
|
"enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
|
||||||
"enable_user_webhooks": app.state.config.ENABLE_USER_WEBHOOKS,
|
"enable_user_webhooks": app.state.config.ENABLE_USER_WEBHOOKS,
|
||||||
|
"enable_user_status": app.state.config.ENABLE_USER_STATUS,
|
||||||
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
||||||
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
||||||
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
|||||||
@@ -994,6 +994,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
"ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES,
|
"ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES,
|
||||||
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
|
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
|
||||||
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
|
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
|
||||||
|
"ENABLE_USER_STATUS": request.app.state.config.ENABLE_USER_STATUS,
|
||||||
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
|
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||||
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||||
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
|
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
|
||||||
@@ -1019,6 +1020,7 @@ class AdminConfig(BaseModel):
|
|||||||
ENABLE_MEMORIES: bool
|
ENABLE_MEMORIES: bool
|
||||||
ENABLE_NOTES: bool
|
ENABLE_NOTES: bool
|
||||||
ENABLE_USER_WEBHOOKS: bool
|
ENABLE_USER_WEBHOOKS: bool
|
||||||
|
ENABLE_USER_STATUS: bool
|
||||||
PENDING_USER_OVERLAY_TITLE: Optional[str] = None
|
PENDING_USER_OVERLAY_TITLE: Optional[str] = None
|
||||||
PENDING_USER_OVERLAY_CONTENT: Optional[str] = None
|
PENDING_USER_OVERLAY_CONTENT: Optional[str] = None
|
||||||
RESPONSE_WATERMARK: Optional[str] = None
|
RESPONSE_WATERMARK: Optional[str] = None
|
||||||
@@ -1068,6 +1070,7 @@ async def update_admin_config(
|
|||||||
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
|
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
|
||||||
|
|
||||||
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
|
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
|
||||||
|
request.app.state.config.ENABLE_USER_STATUS = form_data.ENABLE_USER_STATUS
|
||||||
|
|
||||||
request.app.state.config.PENDING_USER_OVERLAY_TITLE = (
|
request.app.state.config.PENDING_USER_OVERLAY_TITLE = (
|
||||||
form_data.PENDING_USER_OVERLAY_TITLE
|
form_data.PENDING_USER_OVERLAY_TITLE
|
||||||
@@ -1097,6 +1100,7 @@ async def update_admin_config(
|
|||||||
"ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES,
|
"ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES,
|
||||||
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
|
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
|
||||||
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
|
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
|
||||||
|
"ENABLE_USER_STATUS": request.app.state.config.ENABLE_USER_STATUS,
|
||||||
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
|
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||||
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||||
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
|
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
|
||||||
|
|||||||
@@ -333,8 +333,14 @@ async def update_user_settings_by_session_user(
|
|||||||
|
|
||||||
@router.get("/user/status")
|
@router.get("/user/status")
|
||||||
async def get_user_status_by_session_user(
|
async def get_user_status_by_session_user(
|
||||||
|
request: Request,
|
||||||
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
|
if not request.app.state.config.ENABLE_USER_STATUS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
|
||||||
|
)
|
||||||
user = Users.get_user_by_id(user.id, db=db)
|
user = Users.get_user_by_id(user.id, db=db)
|
||||||
if user:
|
if user:
|
||||||
return user
|
return user
|
||||||
@@ -352,10 +358,16 @@ async def get_user_status_by_session_user(
|
|||||||
|
|
||||||
@router.post("/user/status/update")
|
@router.post("/user/status/update")
|
||||||
async def update_user_status_by_session_user(
|
async def update_user_status_by_session_user(
|
||||||
|
request: Request,
|
||||||
form_data: UserStatus,
|
form_data: UserStatus,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: Session = Depends(get_session),
|
db: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
if not request.app.state.config.ENABLE_USER_STATUS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
|
||||||
|
)
|
||||||
user = Users.get_user_by_id(user.id, db=db)
|
user = Users.get_user_by_id(user.id, db=db)
|
||||||
if user:
|
if user:
|
||||||
user = Users.update_user_status_by_id(user.id, form_data, db=db)
|
user = Users.update_user_status_by_id(user.id, form_data, db=db)
|
||||||
|
|||||||
@@ -757,6 +757,14 @@
|
|||||||
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
|
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('User Status')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={adminConfig.ENABLE_USER_STATUS} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class=" self-center text-xs font-medium mb-2">
|
<div class=" self-center text-xs font-medium mb-2">
|
||||||
{$i18n.t('Response Watermark')}
|
{$i18n.t('Response Watermark')}
|
||||||
|
|||||||
@@ -793,7 +793,7 @@
|
|||||||
{#if $user !== undefined && $user !== null}
|
{#if $user !== undefined && $user !== null}
|
||||||
<UserMenu
|
<UserMenu
|
||||||
role={$user?.role}
|
role={$user?.role}
|
||||||
profile={true}
|
profile={$config?.features?.enable_user_status ?? true}
|
||||||
showActiveUsers={false}
|
showActiveUsers={false}
|
||||||
on:show={(e) => {
|
on:show={(e) => {
|
||||||
if (e.detail === 'archived-chat') {
|
if (e.detail === 'archived-chat') {
|
||||||
@@ -812,6 +812,7 @@
|
|||||||
aria-label={$i18n.t('Open User Profile Menu')}
|
aria-label={$i18n.t('Open User Profile Menu')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_user_status}
|
||||||
<div class="absolute -bottom-0.5 -right-0.5">
|
<div class="absolute -bottom-0.5 -right-0.5">
|
||||||
<span class="relative flex size-2.5">
|
<span class="relative flex size-2.5">
|
||||||
<span
|
<span
|
||||||
@@ -824,6 +825,7 @@
|
|||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UserMenu>
|
</UserMenu>
|
||||||
@@ -1360,7 +1362,7 @@
|
|||||||
{#if $user !== undefined && $user !== null}
|
{#if $user !== undefined && $user !== null}
|
||||||
<UserMenu
|
<UserMenu
|
||||||
role={$user?.role}
|
role={$user?.role}
|
||||||
profile={true}
|
profile={$config?.features?.enable_user_status ?? true}
|
||||||
showActiveUsers={false}
|
showActiveUsers={false}
|
||||||
on:show={(e) => {
|
on:show={(e) => {
|
||||||
if (e.detail === 'archived-chat') {
|
if (e.detail === 'archived-chat') {
|
||||||
@@ -1379,6 +1381,7 @@
|
|||||||
aria-label={$i18n.t('Open User Profile Menu')}
|
aria-label={$i18n.t('Open User Profile Menu')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_user_status}
|
||||||
<div class="absolute -bottom-0.5 -right-0.5">
|
<div class="absolute -bottom-0.5 -right-0.5">
|
||||||
<span class="relative flex size-2.5">
|
<span class="relative flex size-2.5">
|
||||||
<span
|
<span
|
||||||
@@ -1391,6 +1394,7 @@
|
|||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center font-medium">{$user?.name}</div>
|
<div class=" self-center font-medium">{$user?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
<hr class=" border-gray-50/30 dark:border-gray-800/30 my-1.5 p-0" />
|
<hr class=" border-gray-50/30 dark:border-gray-800/30 my-1.5 p-0" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
|
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition cursor-pointer"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user