Merge pull request #23057 from BerriAI/litellm_fix_user_filter_scope

[Fix] User Filter Scope - Make Org-Scoping Opt-In
This commit is contained in:
yuneng-jiang 2026-03-07 18:22:26 -08:00 • committed by GitHub
commit 9e531195ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 499 additions and 69 deletions

View file

@ -30,7 +30,7 @@ from litellm.proxy.management_endpoints.common_daily_activity import (
get_daily_activity,
get_daily_activity_aggregated,
)
from litellm.proxy.auth.auth_checks import get_user_object
from litellm.proxy.auth.auth_checks import get_team_object, get_user_object
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
from litellm.proxy.management_endpoints.key_management_endpoints import (
generate_key_helper_fn,
@ -1820,6 +1820,118 @@ async def add_internal_user_to_organization(
raise Exception(f"Failed to add user to organization: {str(e)}")
async def _resolve_org_filter_for_user_search(
user_api_key_dict: UserAPIKeyAuth,
team_id: Optional[str],
prisma_client: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
) -> Optional[List[str]]:
"""
Return a list of org IDs to filter by, or ``None`` for no filter.
Reads the ``scope_user_search_to_org`` UI-setting flag and applies
role-based access rules when the flag is ON.
"""
from litellm.proxy.management_endpoints.common_utils import (
_is_user_team_admin,
)
from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import (
get_ui_settings_cached,
)
ui_settings = await get_ui_settings_cached()
if not ui_settings.get("scope_user_search_to_org", False):
return None # flag OFF — no filtering
if _user_has_admin_view(user_api_key_dict):
return None # proxy admin — see everything
# Try to resolve org admin memberships
caller_user = None
if user_api_key_dict.user_id is not None:
try:
caller_user = await get_user_object(
user_id=user_api_key_dict.user_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
user_id_upsert=False,
proxy_logging_obj=proxy_logging_obj,
)
except ValueError:
caller_user = None
org_admin_org_ids: List[str] = []
if caller_user is not None:
org_admin_org_ids = [
m.organization_id
for m in (caller_user.organization_memberships or [])
if m.user_role == LitellmUserRoles.ORG_ADMIN.value
]
if org_admin_org_ids:
return org_admin_org_ids
if team_id is not None:
return await _resolve_team_org_filter(
user_api_key_dict, team_id, prisma_client,
user_api_key_cache, proxy_logging_obj,
)
raise HTTPException(
status_code=403,
detail={
"error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users."
},
)
async def _resolve_team_org_filter(
user_api_key_dict: UserAPIKeyAuth,
team_id: str,
prisma_client: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
) -> List[str]:
"""Look up the team and return its org as a filter list, or raise 403."""
from litellm.proxy.management_endpoints.common_utils import (
_is_user_team_admin,
)
try:
team_obj = await get_team_object(
team_id=team_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
proxy_logging_obj=proxy_logging_obj,
)
except HTTPException:
raise HTTPException(
status_code=403,
detail={
"error": f"scope_user_search_to_org is enabled but team '{team_id}' was not found."
},
)
if not _is_user_team_admin(user_api_key_dict, team_obj):
raise HTTPException(
status_code=403,
detail={
"error": "scope_user_search_to_org is enabled. You must be an admin of this team to search users."
},
)
if team_obj.organization_id:
return [team_obj.organization_id]
raise HTTPException(
status_code=403,
detail={
"error": "scope_user_search_to_org is enabled and this team is not part of an organization. Contact your proxy admin to adjust this setting."
},
)
@router.get(
"/user/filter/ui",
tags=["Internal User management"],
@ -1836,6 +1948,10 @@ async def ui_view_users(
user_email: Optional[str] = fastapi.Query(
default=None, description="User email in the request parameters"
),
team_id: Optional[str] = fastapi.Query(
default=None,
description="Team ID — used when a team admin searches for users to add to their team",
),
page: int = fastapi.Query(
default=1, description="Page number for pagination", ge=1
),
@ -1847,19 +1963,15 @@ async def ui_view_users(
"""
Filter users based on partial match of user_id or email with pagination.
- Proxy admins: receive all matching users.
- Organization admins: receive only users in their own organization(s).
- Other roles: access denied (403).
Behaviour depends on the ``scope_user_search_to_org`` UI-setting flag
(stored in the ``litellm_uisettings`` table):
Args:
user_id (Optional[str]): Partial user ID to search for
user_email (Optional[str]): Partial email to search for
page (int): Page number for pagination (starts at 1)
page_size (int): Number of items per page (max 100)
user_api_key_dict (UserAPIKeyAuth): User authentication information
Returns:
List of matching user records (LiteLLM_UserTableFiltered), scoped by org for org admins.
* **Flag OFF (default):** any authenticated user can search all users.
* **Flag ON:**
- Proxy admins see all users.
- Org admins see only users in their org(s).
- Team admins for an org-bound team see users in that org.
- Others receive a 403.
"""
from litellm.proxy.proxy_server import (
prisma_client,
@ -1871,51 +1983,13 @@ async def ui_view_users(
raise HTTPException(status_code=500, detail={"error": "No db connected"})
try:
# Restrict by caller role: proxy admin sees all; org admin sees only their org(s); others 403
is_proxy_admin = _user_has_admin_view(user_api_key_dict)
if not is_proxy_admin:
if user_api_key_dict.user_id is None:
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins and organization admins can search users."
},
)
try:
caller_user = await get_user_object(
user_id=user_api_key_dict.user_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
user_id_upsert=False,
proxy_logging_obj=proxy_logging_obj,
)
except ValueError:
# get_user_object raises ValueError when user not found (user_id_upsert=False)
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins and organization admins can search users."
},
)
if caller_user is None:
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins and organization admins can search users."
},
)
org_admin_org_ids = [
m.organization_id
for m in (caller_user.organization_memberships or [])
if m.user_role == LitellmUserRoles.ORG_ADMIN.value
]
if not org_admin_org_ids:
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins and organization admins can search users."
},
)
org_filter_ids = await _resolve_org_filter_for_user_search(
user_api_key_dict=user_api_key_dict,
team_id=team_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
proxy_logging_obj=proxy_logging_obj,
)
# Calculate offset for pagination
skip = (page - 1) * page_size
@ -1935,10 +2009,10 @@ async def ui_view_users(
"mode": "insensitive", # Case-insensitive search
}
# Org admins: only users in their org(s)
if not is_proxy_admin:
# Apply org filter when scope_user_search_to_org is ON and caller is not proxy admin
if org_filter_ids is not None:
where_conditions["organization_memberships"] = {
"some": {"organization_id": {"in": org_admin_org_ids}}
"some": {"organization_id": {"in": org_filter_ids}}
}
# Query users with pagination and filters

View file

@ -124,6 +124,11 @@ class UISettings(BaseModel):
description="If true, team admins are exempt from the vector stores disable restriction (only takes effect when disable_vector_stores_for_internal_users is true).",
)
scope_user_search_to_org: bool = Field(
default=False,
description="If enabled, the user search endpoint (/user/filter/ui) restricts results by organization. When off, any authenticated user can search all users.",
)
class UISettingsResponse(SettingsResponse):
"""Response model for UI settings"""
@ -143,6 +148,7 @@ ALLOWED_UI_SETTINGS_FIELDS = {
"allow_agents_for_team_admins",
"disable_vector_stores_for_internal_users",
"allow_vector_stores_for_team_admins",
"scope_user_search_to_org",
}
# Flags that must be synced from the persisted UISettings into
@ -974,6 +980,49 @@ async def get_in_product_nudges():
return InProductNudgeResponse(is_claude_code_enabled=False)
UI_SETTINGS_CACHE_KEY = "ui_settings:settings_dict"
UI_SETTINGS_CACHE_TTL = 600 # 10 minutes
async def get_ui_settings_cached() -> Dict[str, Any]:
"""
Return the persisted UI settings dict, using DualCache for reads.
Cache hit → return cached dict immediately.
Cache miss → read from DB, populate cache, return dict.
"""
from litellm.proxy.proxy_server import prisma_client, user_api_key_cache
# 1. Try cache
cached = await user_api_key_cache.async_get_cache(key=UI_SETTINGS_CACHE_KEY)
if cached is not None and isinstance(cached, dict):
return cached
# 2. Fallback to DB
if prisma_client is None:
return {}
db_record = await prisma_client.db.litellm_uisettings.find_unique(
where={"id": "ui_settings"}
)
ui_settings: Dict[str, Any] = {}
if db_record and db_record.ui_settings:
raw = db_record.ui_settings
ui_settings = json.loads(raw) if isinstance(raw, str) else dict(raw)
# Sanitize
ui_settings = {
k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS
}
# 3. Populate cache with TTL
await user_api_key_cache.async_set_cache(
key=UI_SETTINGS_CACHE_KEY, value=ui_settings, ttl=UI_SETTINGS_CACHE_TTL
)
return ui_settings
@router.get(
"/get/ui_settings",
tags=["UI Settings"],
@ -1018,6 +1067,13 @@ async def get_ui_settings():
general_settings.update(_flags_to_sync)
# Refresh DualCache so other code paths (e.g. /user/filter/ui) see fresh values
from litellm.proxy.proxy_server import user_api_key_cache
await user_api_key_cache.async_set_cache(
key=UI_SETTINGS_CACHE_KEY, value=ui_settings, ttl=UI_SETTINGS_CACHE_TTL
)
# Build config-like object for schema helper
config: Dict[str, Any] = {"litellm_settings": {"ui_settings": ui_settings}}
@ -1102,6 +1158,16 @@ async def update_ui_settings(
general_settings.update(_flags_to_sync)
# Invalidate + set DualCache so subsequent reads see the new values immediately
from litellm.proxy.proxy_server import user_api_key_cache
sanitized = {
k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS
}
await user_api_key_cache.async_set_cache(
key=UI_SETTINGS_CACHE_KEY, value=sanitized, ttl=UI_SETTINGS_CACHE_TTL
)
return {
"message": "UI settings updated successfully",
"status": "success",

View file

@ -54,6 +54,12 @@ async def test_ui_view_users_with_null_email(mocker, caplog):
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
# Flag OFF by default
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={},
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
# Proxy admin: no org filter, no get_user_object call
@ -63,6 +69,7 @@ async def test_ui_view_users_with_null_email(mocker, caplog):
),
user_id="test_user",
user_email=None,
team_id=None,
page=1,
page_size=50,
)
@ -83,6 +90,12 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker):
return []
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
# Flag OFF by default
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={},
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
await ui_view_users(
@ -91,6 +104,7 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker):
),
user_id=None,
user_email="foo",
team_id=None,
page=1,
page_size=50,
)
@ -99,8 +113,8 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker):
@pytest.mark.asyncio
async def test_ui_view_users_org_admin_filtered_by_org(mocker):
"""
Org admin: find_many is called with organization_memberships filter so only users
in the caller's org(s) are returned.
Org admin with scope_user_search_to_org ON: find_many is called with
organization_memberships filter so only users in the caller's org(s) are returned.
"""
from litellm.proxy._types import LiteLLM_OrganizationMembershipTable
@ -116,6 +130,13 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker):
return []
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
# Flag ON
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={"scope_user_search_to_org": True},
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
@ -143,6 +164,7 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker):
user_api_key_dict=UserAPIKeyAuth(user_id="org-admin", user_role=None),
user_id=None,
user_email="u",
team_id=None,
page=1,
page_size=50,
)
@ -153,11 +175,18 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker):
@pytest.mark.asyncio
async def test_ui_view_users_non_org_admin_returns_403(mocker):
"""
Caller is not proxy admin and not org admin: endpoint returns 403.
Flag ON, caller is not proxy admin and not org admin, no team_id: endpoint returns 403.
"""
from fastapi import HTTPException
mock_prisma_client = mocker.MagicMock()
# Flag ON
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={"scope_user_search_to_org": True},
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
@ -179,12 +208,227 @@ async def test_ui_view_users_non_org_admin_returns_403(mocker):
user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None),
user_id=None,
user_email="u",
team_id=None,
page=1,
page_size=50,
)
assert exc_info.value.status_code == 403
assert "Only proxy admins and organization admins" in str(exc_info.value.detail)
assert "scope_user_search_to_org is enabled" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_ui_view_users_flag_off_internal_user_can_search(mocker):
"""
Flag OFF (default): any authenticated user can search all users without org filtering.
"""
mock_prisma_client = mocker.MagicMock()
async def mock_find_many(*args, **kwargs):
where = kwargs.get("where") or {}
assert "organization_memberships" not in where
return []
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
# Flag OFF
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={},
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
response = await ui_view_users(
user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None),
user_id=None,
user_email="foo",
team_id=None,
page=1,
page_size=50,
)
assert response == []
@pytest.mark.asyncio
async def test_ui_view_users_flag_on_team_admin_org_team(mocker):
"""
Flag ON, team admin for org-bound team: org filter is applied using team's org.
"""
from litellm.proxy._types import LiteLLM_TeamTableCachedObj
mock_prisma_client = mocker.MagicMock()
org_id = "org-456"
tid = "team-789"
async def mock_find_many(*args, **kwargs):
where = kwargs.get("where") or {}
assert "organization_memberships" in where
assert where["organization_memberships"] == {
"some": {"organization_id": {"in": [org_id]}}
}
return []
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
# Flag ON
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={"scope_user_search_to_org": True},
)
# Mock get_team_object
team_obj = LiteLLM_TeamTableCachedObj(
team_id=tid,
team_alias="test-team",
organization_id=org_id,
members_with_roles=[{"user_id": "team-admin-user", "role": "admin"}],
)
async def mock_get_team_object(*args, **kwargs):
return team_obj
mocker.patch(
"litellm.proxy.management_endpoints.internal_user_endpoints.get_team_object",
side_effect=mock_get_team_object,
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
# Caller is not org admin
caller_user = mocker.MagicMock()
caller_user.organization_memberships = []
async def mock_get_user_object(*args, **kwargs):
return caller_user
mocker.patch(
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object",
side_effect=mock_get_user_object,
)
response = await ui_view_users(
user_api_key_dict=UserAPIKeyAuth(user_id="team-admin-user", user_role=None),
user_id=None,
user_email="u",
team_id=tid,
page=1,
page_size=50,
)
assert response == []
@pytest.mark.asyncio
async def test_ui_view_users_flag_on_team_admin_non_org_team_403(mocker):
"""
Flag ON, team admin for non-org team: returns 403.
"""
from fastapi import HTTPException
from litellm.proxy._types import LiteLLM_TeamTableCachedObj
mock_prisma_client = mocker.MagicMock()
tid = "team-no-org"
# Flag ON
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={"scope_user_search_to_org": True},
)
# Mock get_team_object — team has no organization_id
team_obj = LiteLLM_TeamTableCachedObj(
team_id=tid,
team_alias="no-org-team",
organization_id=None,
members_with_roles=[{"user_id": "team-admin-user", "role": "admin"}],
)
async def mock_get_team_object(*args, **kwargs):
return team_obj
mocker.patch(
"litellm.proxy.management_endpoints.internal_user_endpoints.get_team_object",
side_effect=mock_get_team_object,
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
# Caller is not org admin
caller_user = mocker.MagicMock()
caller_user.organization_memberships = []
async def mock_get_user_object(*args, **kwargs):
return caller_user
mocker.patch(
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object",
side_effect=mock_get_user_object,
)
with pytest.raises(HTTPException) as exc_info:
await ui_view_users(
user_api_key_dict=UserAPIKeyAuth(
user_id="team-admin-user", user_role=None
),
user_id=None,
user_email="u",
team_id=tid,
page=1,
page_size=50,
)
assert exc_info.value.status_code == 403
assert "not part of an organization" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_ui_view_users_flag_on_non_admin_no_team_id_403(mocker):
"""
Flag ON, non-admin caller without team_id: returns 403.
"""
from fastapi import HTTPException
mock_prisma_client = mocker.MagicMock()
# Flag ON
mocker.patch(
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
return_value={"scope_user_search_to_org": True},
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
# Caller is not org admin
caller_user = mocker.MagicMock()
caller_user.organization_memberships = []
async def mock_get_user_object(*args, **kwargs):
return caller_user
mocker.patch(
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object",
side_effect=mock_get_user_object,
)
with pytest.raises(HTTPException) as exc_info:
await ui_view_users(
user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None),
user_id=None,
user_email="u",
team_id=None,
page=1,
page_size=50,
)
assert exc_info.value.status_code == 403
assert "scope_user_search_to_org is enabled" in str(exc_info.value.detail)
def test_user_daily_activity_types():

View file

@ -23,6 +23,7 @@ export default function UISettings() {
const allowAgentsTeamAdminsProperty = schema?.properties?.allow_agents_for_team_admins;
const disableVectorStoresProperty = schema?.properties?.disable_vector_stores_for_internal_users;
const allowVectorStoresTeamAdminsProperty = schema?.properties?.allow_vector_stores_for_team_admins;
const scopeUserSearchProperty = schema?.properties?.scope_user_search_to_org;
const values = data?.values ?? {};
const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users);
const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user);
@ -167,6 +168,20 @@ export default function UISettings() {
);
};
const handleToggleScopeUserSearch = (checked: boolean) => {
updateSettings(
{ scope_user_search_to_org: checked },
{
onSuccess: () => {
NotificationManager.success("UI settings updated successfully");
},
onError: (error) => {
NotificationManager.fromBackend(error);
},
},
);
};
return (
<Card title="UI Settings">
{isLoading ? (
@ -347,6 +362,26 @@ export default function UISettings() {
<Divider />
{/* Scope user search to organization */}
<Space align="start" size="middle">
<Switch
checked={Boolean(values.scope_user_search_to_org)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleScopeUserSearch}
aria-label={scopeUserSearchProperty?.description ?? "Scope user search to organization"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Scope user search to organization</Typography.Text>
<Typography.Text type="secondary">
{scopeUserSearchProperty?.description ??
"If enabled, the user search endpoint restricts results by organization. When off, any authenticated user can search all users."}
</Typography.Text>
</Space>
</Space>
<Divider />
{/* Page Visibility for Internal Users */}
<PageVisibilitySettings
enabledPagesInternalUsers={values.enabled_ui_pages_internal_users}

View file

@ -35,6 +35,7 @@ interface UserSearchModalProps {
title?: string;
roles?: Role[];
defaultRole?: string;
teamId?: string;
}
const UserSearchModal: React.FC<UserSearchModalProps> = ({
@ -52,6 +53,7 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
{ label: "user", value: "user", description: "User role. Can view team info, but not manage it." },
],
defaultRole = "user",
teamId,
}) => {
const [form] = Form.useForm<FormValues>();
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
@ -69,6 +71,9 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
try {
const params = new URLSearchParams();
params.append(fieldName, searchText);
if (teamId) {
params.append("team_id", teamId);
}
if (accessToken == null) {
return;
}

View file

@ -2473,14 +2473,19 @@ export const allEndUsersCall = async (accessToken: string) => {
export const userFilterUICall = async (accessToken: string, params: URLSearchParams) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`;
const base = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`;
const queryParams = new URLSearchParams();
if (params.get("user_email")) {
url += `?user_email=${params.get("user_email")}`;
queryParams.append("user_email", params.get("user_email")!);
}
if (params.get("user_id")) {
url += `?user_id=${params.get("user_id")}`;
queryParams.append("user_id", params.get("user_id")!);
}
if (params.get("team_id")) {
queryParams.append("team_id", params.get("team_id")!);
}
const qs = queryParams.toString();
const url = qs ? `${base}?${qs}` : base;
const response = await fetch(url, {
method: "GET",

View file

@ -1303,6 +1303,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
onCancel={() => setIsAddMemberModalVisible(false)}
onSubmit={handleMemberCreate}
accessToken={accessToken}
teamId={teamId}
/>
{/* Delete Member Confirmation Modal */}