Browse Source

remove ui config

Dieu 2 months ago
parent
commit
39bcee3f7b

+ 0 - 217
backend/open_webui/routers/configs.py

@@ -328,220 +328,3 @@ async def get_banners(
     return request.app.state.config.BANNERS
 
 
-############################
-# SCIM Configuration
-############################
-
-
-class SCIMConfigForm(BaseModel):
-    enabled: bool
-    token: Optional[str] = None
-    token_created_at: Optional[str] = None
-    token_expires_at: Optional[str] = None
-
-
-class SCIMTokenRequest(BaseModel):
-    expires_in: Optional[int] = None  # seconds until expiration, None = never
-
-
-class SCIMTokenResponse(BaseModel):
-    token: str
-    created_at: str
-    expires_at: Optional[str] = None
-
-
-class SCIMStats(BaseModel):
-    total_users: int
-    total_groups: int
-    last_sync: Optional[str] = None
-
-
-# In-memory storage for SCIM tokens (in production, use database)
-scim_tokens = {}
-
-
-def generate_scim_token(length: int = 48) -> str:
-    """Generate a secure random token for SCIM authentication"""
-    alphabet = string.ascii_letters + string.digits + "-_"
-    return "".join(secrets.choice(alphabet) for _ in range(length))
-
-
-@router.get("/scim", response_model=SCIMConfigForm)
-async def get_scim_config(request: Request, user=Depends(get_admin_user)):
-    """Get current SCIM configuration"""
-    # Get token info from storage
-    token_info = None
-    scim_token = getattr(request.app.state.config, "SCIM_TOKEN", None)
-    # Handle both PersistentConfig and direct value
-    if hasattr(scim_token, 'value'):
-        scim_token = scim_token.value
-    
-    if scim_token and scim_token in scim_tokens:
-        token_info = scim_tokens[scim_token]
-    
-    scim_enabled = getattr(request.app.state.config, "SCIM_ENABLED", False)
-    print(f"Getting SCIM config - raw SCIM_ENABLED: {scim_enabled}, type: {type(scim_enabled)}")
-    # Handle both PersistentConfig and direct value
-    if hasattr(scim_enabled, 'value'):
-        scim_enabled = scim_enabled.value
-    
-    print(f"Returning SCIM config: enabled={scim_enabled}, token={'set' if scim_token else 'not set'}")
-    
-    return SCIMConfigForm(
-        enabled=scim_enabled,
-        token="***" if scim_token else None,  # Don't expose actual token
-        token_created_at=token_info.get("created_at") if token_info else None,
-        token_expires_at=token_info.get("expires_at") if token_info else None,
-    )
-
-
-@router.post("/scim", response_model=SCIMConfigForm)
-async def update_scim_config(request: Request, config: SCIMConfigForm, user=Depends(get_admin_user)):
-    """Update SCIM configuration"""
-    if not WEBUI_AUTH:
-        raise HTTPException(400, detail="Authentication must be enabled for SCIM")
-    
-    print(f"Updating SCIM config: enabled={config.enabled}")
-    
-    # Import here to avoid circular import
-    from open_webui.config import save_config, get_config
-    
-    # Get current config data
-    config_data = get_config()
-    
-    # Update SCIM settings in config data
-    if "scim" not in config_data:
-        config_data["scim"] = {}
-    
-    config_data["scim"]["enabled"] = config.enabled
-    
-    # Save config to database
-    save_config(config_data)
-    
-    # Also update the runtime config
-    scim_enabled_attr = getattr(request.app.state.config, "SCIM_ENABLED", None)
-    if scim_enabled_attr:
-        if hasattr(scim_enabled_attr, 'value'):
-            # It's a PersistentConfig object
-            print(f"Updating PersistentConfig SCIM_ENABLED from {scim_enabled_attr.value} to {config.enabled}")
-            scim_enabled_attr.value = config.enabled
-        else:
-            # Direct assignment
-            print(f"Direct assignment SCIM_ENABLED to {config.enabled}")
-            request.app.state.config.SCIM_ENABLED = config.enabled
-    else:
-        # Create if doesn't exist
-        print(f"Creating SCIM_ENABLED with value {config.enabled}")
-        request.app.state.config.SCIM_ENABLED = config.enabled
-    
-    # Return updated config
-    return await get_scim_config(request=request, user=user)
-
-
-@router.post("/scim/token", response_model=SCIMTokenResponse)
-async def generate_scim_token_endpoint(
-    request: Request, token_request: SCIMTokenRequest, user=Depends(get_admin_user)
-):
-    """Generate a new SCIM bearer token"""
-    token = generate_scim_token()
-    created_at = datetime.utcnow()
-    expires_at = None
-    
-    if token_request.expires_in:
-        expires_at = created_at + timedelta(seconds=token_request.expires_in)
-    
-    # Store token info
-    token_info = {
-        "token": token,
-        "created_at": created_at.isoformat(),
-        "expires_at": expires_at.isoformat() if expires_at else None,
-    }
-    scim_tokens[token] = token_info
-    
-    # Import here to avoid circular import
-    from open_webui.config import save_config, get_config
-    
-    # Get current config data
-    config_data = get_config()
-    
-    # Update SCIM token in config data
-    if "scim" not in config_data:
-        config_data["scim"] = {}
-    
-    config_data["scim"]["token"] = token
-    
-    # Save config to database
-    save_config(config_data)
-    
-    # Also update the runtime config
-    scim_token_attr = getattr(request.app.state.config, "SCIM_TOKEN", None)
-    if scim_token_attr:
-        if hasattr(scim_token_attr, 'value'):
-            # It's a PersistentConfig object
-            scim_token_attr.value = token
-        else:
-            # Direct assignment
-            request.app.state.config.SCIM_TOKEN = token
-    else:
-        # Create if doesn't exist
-        request.app.state.config.SCIM_TOKEN = token
-    
-    return SCIMTokenResponse(
-        token=token,
-        created_at=token_info["created_at"],
-        expires_at=token_info["expires_at"],
-    )
-
-
-@router.delete("/scim/token")
-async def revoke_scim_token(request: Request, user=Depends(get_admin_user)):
-    """Revoke the current SCIM token"""
-    # Get current token
-    scim_token = getattr(request.app.state.config, "SCIM_TOKEN", None)
-    if hasattr(scim_token, 'value'):
-        scim_token = scim_token.value
-    
-    # Remove from storage
-    if scim_token and scim_token in scim_tokens:
-        del scim_tokens[scim_token]
-    
-    # Import here to avoid circular import
-    from open_webui.config import save_config, get_config
-    
-    # Get current config data
-    config_data = get_config()
-    
-    # Remove SCIM token from config data
-    if "scim" in config_data:
-        config_data["scim"]["token"] = None
-    
-    # Save config to database
-    save_config(config_data)
-    
-    # Also update the runtime config
-    scim_token_attr = getattr(request.app.state.config, "SCIM_TOKEN", None)
-    if scim_token_attr:
-        if hasattr(scim_token_attr, 'value'):
-            # It's a PersistentConfig object
-            scim_token_attr.value = None
-        else:
-            # Direct assignment
-            request.app.state.config.SCIM_TOKEN = None
-    
-    return {"detail": "SCIM token revoked successfully"}
-
-
-@router.get("/scim/stats", response_model=SCIMStats)
-async def get_scim_stats(request: Request, user=Depends(get_admin_user)):
-    """Get SCIM statistics"""
-    users = Users.get_users()
-    groups = Groups.get_groups()
-    
-    # Get last sync time (in production, track this properly)
-    last_sync = None
-    
-    return SCIMStats(
-        total_users=len(users),
-        total_groups=len(groups) if groups else 0,
-        last_sync=last_sync,
-    )

+ 0 - 200
src/lib/apis/scim/index.ts

@@ -1,200 +0,0 @@
-import { WEBUI_API_BASE_URL } from '$lib/constants';
-
-// SCIM API endpoints
-const SCIM_BASE_URL = `${WEBUI_API_BASE_URL}/scim/v2`;
-
-export interface SCIMConfig {
-	enabled: boolean;
-	token?: string;
-	token_created_at?: string;
-	token_expires_at?: string;
-}
-
-export interface SCIMStats {
-	total_users: number;
-	total_groups: number;
-	last_sync?: string;
-}
-
-export interface SCIMToken {
-	token: string;
-	created_at: string;
-	expires_at?: string;
-}
-
-// Get SCIM configuration
-export const getSCIMConfig = async (token: string): Promise<SCIMConfig> => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/scim`, {
-		method: 'GET',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${token}`
-		}
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return res.json();
-		})
-		.catch((err) => {
-			console.error(err);
-			error = err.detail;
-			return null;
-		});
-
-	if (error) {
-		throw error;
-	}
-
-	return res;
-};
-
-// Update SCIM configuration
-export const updateSCIMConfig = async (token: string, config: Partial<SCIMConfig>): Promise<SCIMConfig> => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/scim`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify(config)
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return res.json();
-		})
-		.catch((err) => {
-			console.error(err);
-			error = err.detail;
-			return null;
-		});
-
-	if (error) {
-		throw error;
-	}
-
-	return res;
-};
-
-// Generate new SCIM token
-export const generateSCIMToken = async (token: string, expiresIn?: number): Promise<SCIMToken> => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/scim/token`, {
-		method: 'POST',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${token}`
-		},
-		body: JSON.stringify({ expires_in: expiresIn })
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return res.json();
-		})
-		.catch((err) => {
-			console.error(err);
-			error = err.detail;
-			return null;
-		});
-
-	if (error) {
-		throw error;
-	}
-
-	return res;
-};
-
-// Revoke SCIM token
-export const revokeSCIMToken = async (token: string): Promise<boolean> => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/scim/token`, {
-		method: 'DELETE',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${token}`
-		}
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return true;
-		})
-		.catch((err) => {
-			console.error(err);
-			error = err.detail;
-			return false;
-		});
-
-	if (error) {
-		throw error;
-	}
-
-	return res;
-};
-
-// Get SCIM statistics
-export const getSCIMStats = async (token: string): Promise<SCIMStats> => {
-	let error = null;
-
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/scim/stats`, {
-		method: 'GET',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${token}`
-		}
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return res.json();
-		})
-		.catch((err) => {
-			console.error(err);
-			error = err.detail;
-			return null;
-		});
-
-	if (error) {
-		throw error;
-	}
-
-	return res;
-};
-
-// Test SCIM connection
-export const testSCIMConnection = async (token: string, scimToken: string): Promise<boolean> => {
-	let error = null;
-
-	// Test by calling the SCIM service provider config endpoint
-	const res = await fetch(`${SCIM_BASE_URL}/ServiceProviderConfig`, {
-		method: 'GET',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			Authorization: `Bearer ${scimToken}`
-		}
-	})
-		.then(async (res) => {
-			if (!res.ok) throw await res.json();
-			return true;
-		})
-		.catch((err) => {
-			console.error(err);
-			error = err.detail || 'Connection failed';
-			return false;
-		});
-
-	if (error) {
-		throw error;
-	}
-
-	return res;
-};

+ 0 - 35
src/lib/components/admin/Settings.svelte

@@ -15,7 +15,6 @@
 	import Interface from './Settings/Interface.svelte';
 	import Models from './Settings/Models.svelte';
 	import Connections from './Settings/Connections.svelte';
-	import SCIM from './Settings/SCIM.svelte';
 	import Documents from './Settings/Documents.svelte';
 	import WebSearch from './Settings/WebSearch.svelte';
 
@@ -36,7 +35,6 @@
 		selectedTab = [
 			'general',
 			'connections',
-			'scim',
 			'models',
 			'evaluations',
 			'tools',
@@ -139,30 +137,6 @@
 			<div class=" self-center">{$i18n.t('Connections')}</div>
 		</button>
 
-		<button
-			id="scim"
-			class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
-			'scim'
-				? ''
-				: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
-			on:click={() => {
-				goto('/admin/settings/scim');
-			}}
-		>
-			<div class=" self-center mr-2">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 16 16"
-					fill="currentColor"
-					class="w-4 h-4"
-				>
-					<path
-						d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z"
-					/>
-				</svg>
-			</div>
-			<div class=" self-center">{$i18n.t('SCIM')}</div>
-		</button>
 
 		<button
 			id="models"
@@ -476,15 +450,6 @@
 					toast.success($i18n.t('Settings saved successfully!'));
 				}}
 			/>
-		{:else if selectedTab === 'scim'}
-			<SCIM
-				saveHandler={async () => {
-					toast.success($i18n.t('Settings saved successfully!'));
-
-					await tick();
-					await config.set(await getBackendConfig());
-				}}
-			/>
 		{:else if selectedTab === 'models'}
 			<Models />
 		{:else if selectedTab === 'evaluations'}

+ 0 - 364
src/lib/components/admin/Settings/SCIM.svelte

@@ -1,364 +0,0 @@
-<script lang="ts">
-	import { onMount, getContext } from 'svelte';
-	import { toast } from 'svelte-sonner';
-	import { copyToClipboard } from '$lib/utils';
-	
-	import {
-		getSCIMConfig,
-		updateSCIMConfig,
-		generateSCIMToken,
-		revokeSCIMToken,
-		getSCIMStats,
-		testSCIMConnection,
-		type SCIMConfig,
-		type SCIMStats
-	} from '$lib/apis/scim';
-
-	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
-	import Spinner from '$lib/components/common/Spinner.svelte';
-	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import Switch from '$lib/components/common/Switch.svelte';
-	import Badge from '$lib/components/common/Badge.svelte';
-	
-	const i18n = getContext('i18n');
-
-	export let saveHandler: () => void;
-	
-	let loading = false;
-	let testingConnection = false;
-	let generatingToken = false;
-	
-	let scimEnabled = false;
-	let scimToken = '';
-	let scimTokenCreatedAt = '';
-	let scimTokenExpiresAt = '';
-	let showToken = false;
-	let tokenExpiry = 'never'; // 'never', '30days', '90days', '1year'
-	
-	let scimStats: SCIMStats | null = null;
-	let scimBaseUrl = '';
-
-	// Generate SCIM base URL
-	// In production, the frontend and backend are served from the same origin
-	// In development, we need to show the backend URL
-	$: {
-		if (import.meta.env.DEV) {
-			// Development mode - backend is on port 8080
-			scimBaseUrl = `http://localhost:8080/api/v1/scim/v2`;
-		} else {
-			// Production mode - same origin
-			scimBaseUrl = `${window.location.origin}/api/v1/scim/v2`;
-		}
-	}
-
-	const formatDate = (dateString: string) => {
-		if (!dateString) return 'N/A';
-		return new Date(dateString).toLocaleString();
-	};
-
-	const loadSCIMConfig = async () => {
-		loading = true;
-		try {
-			const config = await getSCIMConfig(localStorage.token);
-			console.log('Loaded SCIM config:', config);
-			scimEnabled = config.enabled || false;
-			scimToken = config.token || '';
-			scimTokenCreatedAt = config.token_created_at || '';
-			scimTokenExpiresAt = config.token_expires_at || '';
-			
-			if (scimEnabled && scimToken) {
-				try {
-					scimStats = await getSCIMStats(localStorage.token);
-				} catch (statsError) {
-					console.error('Error loading SCIM stats:', statsError);
-					// Don't fail the whole load if stats fail
-				}
-			}
-		} catch (error) {
-			console.error('Error loading SCIM config:', error);
-			toast.error($i18n.t('Failed to load SCIM configuration'));
-		} finally {
-			loading = false;
-		}
-	};
-
-	const handleToggleSCIM = async () => {
-		loading = true;
-		try {
-			console.log('Updating SCIM config, enabled:', scimEnabled);
-			const config = await updateSCIMConfig(localStorage.token, { enabled: scimEnabled });
-			console.log('SCIM config updated:', config);
-			toast.success($i18n.t('SCIM configuration updated'));
-			
-			if (scimEnabled && !scimToken) {
-				toast.info($i18n.t('Please generate a SCIM token to enable provisioning'));
-			}
-			
-			// Reload config to ensure it's synced
-			await loadSCIMConfig();
-			
-			saveHandler();
-		} catch (error) {
-			console.error('Error updating SCIM config:', error);
-			toast.error($i18n.t('Failed to update SCIM configuration') + ': ' + (error.message || error));
-			// Revert toggle
-			scimEnabled = !scimEnabled;
-		} finally {
-			loading = false;
-		}
-	};
-
-	const handleGenerateToken = async () => {
-		generatingToken = true;
-		try {
-			let expiresIn = null;
-			switch (tokenExpiry) {
-				case '30days':
-					expiresIn = 30 * 24 * 60 * 60; // 30 days in seconds
-					break;
-				case '90days':
-					expiresIn = 90 * 24 * 60 * 60; // 90 days in seconds
-					break;
-				case '1year':
-					expiresIn = 365 * 24 * 60 * 60; // 1 year in seconds
-					break;
-			}
-			
-			const tokenData = await generateSCIMToken(localStorage.token, expiresIn);
-			scimToken = tokenData.token;
-			scimTokenCreatedAt = tokenData.created_at;
-			scimTokenExpiresAt = tokenData.expires_at || '';
-			showToken = true;
-			
-			toast.success($i18n.t('SCIM token generated successfully'));
-			toast.info($i18n.t('Make sure to copy this token now. You won\'t be able to see it again!'));
-		} catch (error) {
-			console.error('Error generating SCIM token:', error);
-			toast.error($i18n.t('Failed to generate SCIM token'));
-		} finally {
-			generatingToken = false;
-		}
-	};
-
-	const handleRevokeToken = async () => {
-		if (!confirm($i18n.t('Are you sure you want to revoke the SCIM token? This will break any existing integrations.'))) {
-			return;
-		}
-		
-		loading = true;
-		try {
-			await revokeSCIMToken(localStorage.token);
-			scimToken = '';
-			scimTokenCreatedAt = '';
-			scimTokenExpiresAt = '';
-			showToken = false;
-			
-			toast.success($i18n.t('SCIM token revoked successfully'));
-		} catch (error) {
-			console.error('Error revoking SCIM token:', error);
-			toast.error($i18n.t('Failed to revoke SCIM token'));
-		} finally {
-			loading = false;
-		}
-	};
-
-	const handleTestConnection = async () => {
-		testingConnection = true;
-		try {
-			const success = await testSCIMConnection(localStorage.token, scimToken);
-			if (success) {
-				toast.success($i18n.t('SCIM endpoint is accessible'));
-			} else {
-				toast.error($i18n.t('SCIM endpoint is not accessible'));
-			}
-		} catch (error) {
-			console.error('Error testing SCIM connection:', error);
-			toast.error($i18n.t('Failed to test SCIM connection'));
-		} finally {
-			testingConnection = false;
-		}
-	};
-
-	const copyTokenToClipboard = () => {
-		copyToClipboard(scimToken);
-		toast.success($i18n.t('Token copied to clipboard'));
-	};
-
-	const copySCIMUrlToClipboard = () => {
-		copyToClipboard(scimBaseUrl);
-		toast.success($i18n.t('SCIM URL copied to clipboard'));
-	};
-
-	onMount(() => {
-		loadSCIMConfig();
-	});
-</script>
-
-<div class="flex flex-col gap-4 px-1 py-3 md:py-3">
-	<div class="flex items-center justify-between">
-		<div class="flex items-center gap-2">
-			<h3 class="text-lg font-semibold">{$i18n.t('SCIM 2.0 Integration')}</h3>
-			<Badge type="info">Enterprise</Badge>
-		</div>
-		
-		<Switch bind:state={scimEnabled} on:change={handleToggleSCIM} disabled={loading} />
-	</div>
-
-	<div class="text-sm text-gray-500 dark:text-gray-400">
-		{$i18n.t('Enable SCIM 2.0 support for automated user and group provisioning from identity providers like Okta, Azure AD, and Google Workspace.')}
-	</div>
-
-	{#if scimEnabled}
-		<div class="space-y-4 mt-4">
-			<!-- Save Button -->
-			<div class="flex justify-end">
-				<button
-					type="button"
-					on:click={saveHandler}
-					class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-lg"
-				>
-					{$i18n.t('Save')}
-				</button>
-			</div>
-			
-			<!-- SCIM Base URL -->
-			<div>
-				<label class="block text-sm font-medium mb-2">{$i18n.t('SCIM Base URL')}</label>
-				<div class="flex items-center gap-2">
-					<input
-						type="text"
-						value={scimBaseUrl}
-						readonly
-						class="flex-1 px-3 py-2 text-sm rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
-					/>
-					<button
-						type="button"
-						on:click={copySCIMUrlToClipboard}
-						class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition"
-						title={$i18n.t('Copy URL')}
-					>
-						<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
-							<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
-						</svg>
-					</button>
-				</div>
-				<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
-					{$i18n.t('Use this URL in your identity provider\'s SCIM configuration')}
-				</p>
-			</div>
-
-			<!-- SCIM Token -->
-			<div>
-				<label class="block text-sm font-medium mb-2">{$i18n.t('SCIM Bearer Token')}</label>
-				
-				{#if scimToken}
-					<div class="space-y-2">
-						<div class="flex items-center gap-2">
-							<SensitiveInput
-								bind:value={scimToken}
-								bind:show={showToken}
-								readonly
-								placeholder={$i18n.t('Token hidden for security')}
-							/>
-							<button
-								type="button"
-								on:click={copyTokenToClipboard}
-								class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition"
-								title={$i18n.t('Copy token')}
-							>
-								<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
-									<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
-								</svg>
-							</button>
-						</div>
-						
-						<div class="text-xs text-gray-500 dark:text-gray-400">
-							<p>{$i18n.t('Created')}: {formatDate(scimTokenCreatedAt)}</p>
-							{#if scimTokenExpiresAt}
-								<p>{$i18n.t('Expires')}: {formatDate(scimTokenExpiresAt)}</p>
-							{:else}
-								<p>{$i18n.t('Expires')}: {$i18n.t('Never')}</p>
-							{/if}
-						</div>
-						
-						<div class="flex gap-2">
-							<button
-								type="button"
-								on:click={handleTestConnection}
-								disabled={testingConnection}
-								class="px-3 py-1.5 text-sm font-medium bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg"
-							>
-								{#if testingConnection}
-									<Spinner size="sm" />
-								{:else}
-									{$i18n.t('Test Connection')}
-								{/if}
-							</button>
-							
-							<button
-								type="button"
-								on:click={handleRevokeToken}
-								disabled={loading}
-								class="px-3 py-1.5 text-sm font-medium bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30 text-red-600 dark:text-red-400 transition rounded-lg"
-							>
-								{$i18n.t('Revoke Token')}
-							</button>
-						</div>
-					</div>
-				{:else}
-					<div class="space-y-3">
-						<div>
-							<label class="block text-sm font-medium mb-1">{$i18n.t('Token Expiration')}</label>
-							<select
-								bind:value={tokenExpiry}
-								class="w-full px-3 py-2 text-sm rounded-lg bg-gray-100 dark:bg-gray-800"
-							>
-								<option value="never">{$i18n.t('Never expire')}</option>
-								<option value="30days">{$i18n.t('30 days')}</option>
-								<option value="90days">{$i18n.t('90 days')}</option>
-								<option value="1year">{$i18n.t('1 year')}</option>
-							</select>
-						</div>
-						
-						<button
-							type="button"
-							on:click={handleGenerateToken}
-							disabled={generatingToken}
-							class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-lg"
-						>
-							{#if generatingToken}
-								<Spinner size="sm" />
-							{:else}
-								{$i18n.t('Generate Token')}
-							{/if}
-						</button>
-					</div>
-				{/if}
-			</div>
-
-			<!-- SCIM Statistics -->
-			{#if scimStats}
-				<div class="border-t pt-4">
-					<h4 class="text-sm font-medium mb-2">{$i18n.t('SCIM Statistics')}</h4>
-					<div class="grid grid-cols-2 gap-4 text-sm">
-						<div>
-							<span class="text-gray-500 dark:text-gray-400">{$i18n.t('Total Users')}:</span>
-							<span class="ml-2 font-medium">{scimStats.total_users}</span>
-						</div>
-						<div>
-							<span class="text-gray-500 dark:text-gray-400">{$i18n.t('Total Groups')}:</span>
-							<span class="ml-2 font-medium">{scimStats.total_groups}</span>
-						</div>
-						{#if scimStats.last_sync}
-							<div class="col-span-2">
-								<span class="text-gray-500 dark:text-gray-400">{$i18n.t('Last Sync')}:</span>
-								<span class="ml-2 font-medium">{formatDate(scimStats.last_sync)}</span>
-							</div>
-						{/if}
-					</div>
-				</div>
-			{/if}
-
-		</div>
-	{/if}
-</div>