mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
remove ui config
This commit is contained in:
parent
f4d54c518e
commit
39bcee3f7b
4 changed files with 0 additions and 816 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue