mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
9614a5cf84
Replace the static, color-only inline `style={{ ... 'var(--bg-primary)' ... }}` props with the new semantic Tailwind utilities (bg-surface, text-content, border-edge, ...) wherever the result is byte-identical; dynamic/conditional theme styles and hardcoded status colors are left inline. Extract the Atlas country-search autocomplete, the Admin update banner, and two Journey dialogs into their own presentational components to shrink the oversized page files, keeping behaviour and markup identical.
852 lines
46 KiB
TypeScript
852 lines
46 KiB
TypeScript
import Section from './Section'
|
||
import React, { useEffect, useRef, useState } from 'react'
|
||
import { useTranslation } from '../../i18n'
|
||
import { useToast } from '../shared/Toast'
|
||
import { Trash2, Copy, Terminal, Plus, Check, KeyRound, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react'
|
||
import { authApi, oauthApi } from '../../api/client'
|
||
import { useAddonStore } from '../../store/addonStore'
|
||
import PhotoProvidersSection from './PhotoProvidersSection'
|
||
import { ALL_SCOPES } from '../../api/oauthScopes'
|
||
import ScopeGroupPicker from '../OAuth/ScopeGroupPicker'
|
||
|
||
interface OAuthPreset {
|
||
id: string
|
||
label: string
|
||
name: string
|
||
uris: string
|
||
scopes: string[]
|
||
}
|
||
|
||
const OAUTH_PRESETS: OAuthPreset[] = [
|
||
{
|
||
id: 'claude-web',
|
||
label: 'Claude.ai',
|
||
name: 'Claude.ai',
|
||
uris: 'https://claude.ai/api/mcp/auth_callback',
|
||
scopes: ALL_SCOPES.filter(s => !s.includes(':delete')),
|
||
},
|
||
{
|
||
id: 'claude-desktop',
|
||
label: 'Claude Desktop',
|
||
name: 'Claude Desktop',
|
||
uris: 'http://localhost',
|
||
scopes: ALL_SCOPES.filter(s => !s.includes(':delete')),
|
||
},
|
||
{
|
||
id: 'cursor',
|
||
label: 'Cursor',
|
||
name: 'Cursor',
|
||
uris: 'http://localhost',
|
||
scopes: ALL_SCOPES.filter(s => !s.includes(':delete')),
|
||
},
|
||
{
|
||
id: 'vscode',
|
||
label: 'VS Code',
|
||
name: 'VS Code / Copilot',
|
||
uris: 'http://localhost',
|
||
scopes: ALL_SCOPES.filter(s => s.endsWith(':read')),
|
||
},
|
||
{
|
||
id: 'windsurf',
|
||
label: 'Windsurf',
|
||
name: 'Windsurf',
|
||
uris: 'http://localhost',
|
||
scopes: ALL_SCOPES.filter(s => !s.includes(':delete')),
|
||
},
|
||
{
|
||
id: 'zed',
|
||
label: 'Zed',
|
||
name: 'Zed',
|
||
uris: 'http://localhost',
|
||
scopes: ALL_SCOPES.filter(s => !s.includes(':delete')),
|
||
},
|
||
]
|
||
|
||
|
||
interface OAuthClient {
|
||
id: string
|
||
name: string
|
||
client_id: string
|
||
redirect_uris: string[]
|
||
allowed_scopes: string[]
|
||
allows_client_credentials: boolean
|
||
created_at: string
|
||
client_secret?: string // only present on create
|
||
}
|
||
|
||
interface OAuthSession {
|
||
id: number
|
||
client_id: string
|
||
client_name: string
|
||
scopes: string[]
|
||
access_token_expires_at: string
|
||
refresh_token_expires_at: string
|
||
created_at: string
|
||
}
|
||
|
||
interface McpToken {
|
||
id: number
|
||
name: string
|
||
token_prefix: string
|
||
created_at: string
|
||
last_used_at: string | null
|
||
}
|
||
|
||
export default function IntegrationsTab(): React.ReactElement {
|
||
const S = useIntegrations()
|
||
return (
|
||
<>
|
||
<PhotoProvidersSection />
|
||
{S.mcpEnabled && <IntegrationsMcpSection {...S} />}
|
||
<McpTokenModals {...S} />
|
||
<OAuthClientModals {...S} />
|
||
</>
|
||
)
|
||
}
|
||
|
||
function useIntegrations() {
|
||
const { t, locale } = useTranslation()
|
||
const toast = useToast()
|
||
const { isEnabled: addonEnabled, loadAddons } = useAddonStore()
|
||
const mcpEnabled = addonEnabled('mcp')
|
||
|
||
useEffect(() => {
|
||
loadAddons()
|
||
}, [loadAddons])
|
||
|
||
// OAuth clients state
|
||
const [oauthClients, setOauthClients] = useState<OAuthClient[]>([])
|
||
const [oauthSessions, setOauthSessions] = useState<OAuthSession[]>([])
|
||
const [oauthCreateOpen, setOauthCreateOpen] = useState(false)
|
||
const [oauthNewName, setOauthNewName] = useState('')
|
||
const [oauthNewUris, setOauthNewUris] = useState('')
|
||
const [oauthNewScopes, setOauthNewScopes] = useState<string[]>([])
|
||
const [oauthCreating, setOauthCreating] = useState(false)
|
||
const [oauthCreatedClient, setOauthCreatedClient] = useState<OAuthClient | null>(null)
|
||
const [oauthDeleteId, setOauthDeleteId] = useState<string | null>(null)
|
||
const [oauthRevokeId, setOauthRevokeId] = useState<number | null>(null)
|
||
const [oauthRotateId, setOauthRotateId] = useState<string | null>(null)
|
||
const [oauthRotatedSecret, setOauthRotatedSecret] = useState<string | null>(null)
|
||
const [oauthRotating, setOauthRotating] = useState(false)
|
||
// oauthScopesOpen is managed internally by ScopeGroupPicker
|
||
const [oauthScopesExpanded, setOauthScopesExpanded] = useState<Record<string, boolean>>({})
|
||
const [oauthIsMachine, setOauthIsMachine] = useState(false)
|
||
|
||
// MCP sub-tab state
|
||
const [activeMcpTab, setActiveMcpTab] = useState<'oauth' | 'apitokens'>('oauth')
|
||
const [configOpenOAuth, setConfigOpenOAuth] = useState(false)
|
||
const [configOpenToken, setConfigOpenToken] = useState(false)
|
||
|
||
// MCP state
|
||
const [mcpTokens, setMcpTokens] = useState<McpToken[]>([])
|
||
const [mcpModalOpen, setMcpModalOpen] = useState(false)
|
||
const [mcpNewName, setMcpNewName] = useState('')
|
||
const [mcpCreatedToken, setMcpCreatedToken] = useState<string | null>(null)
|
||
const [mcpCreating, setMcpCreating] = useState(false)
|
||
const [mcpDeleteId, setMcpDeleteId] = useState<number | null>(null)
|
||
const [copiedKey, setCopiedKey] = useState<string | null>(null)
|
||
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||
|
||
useEffect(() => {
|
||
return () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) }
|
||
}, [])
|
||
|
||
const mcpEndpoint = `${window.location.origin}/mcp`
|
||
const mcpJsonConfigOAuth = `{
|
||
"mcpServers": {
|
||
"trek": {
|
||
"command": "npx",
|
||
"args": [
|
||
"mcp-remote",
|
||
"${mcpEndpoint}",
|
||
"--static-oauth-client-info",
|
||
"{\\"client_id\\": \\"<your_client_id>\\", \\"client_secret\\": \\"<your_client_secret>\\"}"
|
||
]
|
||
}
|
||
}
|
||
}`
|
||
const mcpJsonConfig = `{
|
||
"mcpServers": {
|
||
"trek": {
|
||
"command": "npx",
|
||
"args": [
|
||
"mcp-remote",
|
||
"${mcpEndpoint}",
|
||
"--header",
|
||
"Authorization: Bearer <your_token>"
|
||
]
|
||
}
|
||
}
|
||
}`
|
||
|
||
useEffect(() => {
|
||
if (mcpEnabled) {
|
||
authApi.mcpTokens.list().then(d => setMcpTokens(d.tokens || [])).catch(() => {})
|
||
}
|
||
}, [mcpEnabled])
|
||
|
||
const handleCreateMcpToken = async () => {
|
||
if (!mcpNewName.trim()) return
|
||
setMcpCreating(true)
|
||
try {
|
||
const d = await authApi.mcpTokens.create(mcpNewName.trim())
|
||
setMcpCreatedToken(d.token.raw_token)
|
||
setMcpNewName('')
|
||
setMcpTokens(prev => [{ id: d.token.id, name: d.token.name, token_prefix: d.token.token_prefix, created_at: d.token.created_at, last_used_at: null }, ...prev])
|
||
} catch {
|
||
toast.error(t('settings.mcp.toast.createError'))
|
||
} finally {
|
||
setMcpCreating(false)
|
||
}
|
||
}
|
||
|
||
const handleDeleteMcpToken = async (id: number) => {
|
||
try {
|
||
await authApi.mcpTokens.delete(id)
|
||
setMcpTokens(prev => prev.filter(tk => tk.id !== id))
|
||
setMcpDeleteId(null)
|
||
toast.success(t('settings.mcp.toast.deleted'))
|
||
} catch {
|
||
toast.error(t('settings.mcp.toast.deleteError'))
|
||
}
|
||
}
|
||
|
||
const handleCopy = (text: string, key: string) => {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
setCopiedKey(key)
|
||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
|
||
copyTimerRef.current = setTimeout(() => setCopiedKey(null), 2000)
|
||
})
|
||
}
|
||
|
||
// Load OAuth clients and sessions
|
||
useEffect(() => {
|
||
if (mcpEnabled) {
|
||
oauthApi.clients.list().then(d => setOauthClients(d.clients || [])).catch(() => {})
|
||
oauthApi.sessions.list().then(d => setOauthSessions(d.sessions || [])).catch(() => {})
|
||
}
|
||
}, [mcpEnabled])
|
||
|
||
const handleCreateOAuthClient = async () => {
|
||
if (!oauthNewName.trim()) return
|
||
if (!oauthIsMachine && !oauthNewUris.trim()) return
|
||
setOauthCreating(true)
|
||
try {
|
||
const uris = oauthIsMachine ? [] : oauthNewUris.split('\n').map(u => u.trim()).filter(Boolean)
|
||
const d = await oauthApi.clients.create({
|
||
name: oauthNewName.trim(),
|
||
redirect_uris: uris,
|
||
allowed_scopes: oauthNewScopes,
|
||
...(oauthIsMachine ? { allows_client_credentials: true } : {}),
|
||
})
|
||
setOauthCreatedClient(d.client)
|
||
setOauthClients(prev => [...prev, { ...d.client, client_secret: undefined }])
|
||
setOauthNewName('')
|
||
setOauthNewUris('')
|
||
setOauthNewScopes([])
|
||
setOauthIsMachine(false)
|
||
} catch {
|
||
toast.error(t('settings.oauth.toast.createError'))
|
||
} finally {
|
||
setOauthCreating(false)
|
||
}
|
||
}
|
||
|
||
const handleDeleteOAuthClient = async (id: string) => {
|
||
try {
|
||
await oauthApi.clients.delete(id)
|
||
setOauthClients(prev => prev.filter(c => c.id !== id))
|
||
setOauthDeleteId(null)
|
||
toast.success(t('settings.oauth.toast.deleted'))
|
||
} catch {
|
||
toast.error(t('settings.oauth.toast.deleteError'))
|
||
}
|
||
}
|
||
|
||
const handleRotateSecret = async (id: string) => {
|
||
setOauthRotating(true)
|
||
try {
|
||
const d = await oauthApi.clients.rotate(id)
|
||
setOauthRotatedSecret(d.client_secret)
|
||
setOauthRotateId(null)
|
||
} catch {
|
||
toast.error(t('settings.oauth.toast.rotateError'))
|
||
} finally {
|
||
setOauthRotating(false)
|
||
}
|
||
}
|
||
|
||
const handleRevokeSession = async (id: number) => {
|
||
try {
|
||
await oauthApi.sessions.revoke(id)
|
||
setOauthSessions(prev => prev.filter(s => s.id !== id))
|
||
setOauthRevokeId(null)
|
||
toast.success(t('settings.oauth.toast.revoked'))
|
||
} catch {
|
||
toast.error(t('settings.oauth.toast.revokeError'))
|
||
}
|
||
}
|
||
|
||
|
||
return {
|
||
t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession,
|
||
}
|
||
}
|
||
|
||
function IntegrationsMcpSection(props: any) {
|
||
const {
|
||
t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession,
|
||
} = props
|
||
return (
|
||
<Section title={t('settings.mcp.title')} icon={Terminal}>
|
||
{/* Endpoint URL */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.mcp.endpoint')}</label>
|
||
<div className="flex items-center gap-2">
|
||
<code className="flex-1 px-3 py-2 rounded-lg text-sm font-mono border bg-surface-secondary border-edge text-content">
|
||
{mcpEndpoint}
|
||
</code>
|
||
<button onClick={() => handleCopy(mcpEndpoint, 'endpoint')}
|
||
className="p-2 rounded-lg border border-edge transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
|
||
title={t('settings.mcp.copy')}>
|
||
{copiedKey === 'endpoint' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4 text-content-secondary" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sub-tab bar */}
|
||
<div className="flex gap-1 rounded-lg p-1 border bg-surface-secondary border-edge">
|
||
<button
|
||
onClick={() => setActiveMcpTab('oauth')}
|
||
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||
activeMcpTab === 'oauth' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
||
}`}>
|
||
{t('settings.oauth.clients')}
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveMcpTab('apitokens')}
|
||
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors flex items-center justify-center gap-2 ${
|
||
activeMcpTab === 'apitokens' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
||
}`}>
|
||
{t('settings.mcp.apiTokens')}
|
||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium"
|
||
style={{ background: 'rgba(245,158,11,0.15)', color: '#b45309', border: '1px solid rgba(245,158,11,0.4)' }}>
|
||
Deprecated
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* OAuth 2.1 Clients tab */}
|
||
{activeMcpTab === 'oauth' && (
|
||
<>
|
||
{/* JSON config — OAuth (collapsible) */}
|
||
<div className="rounded-lg border overflow-hidden border-edge">
|
||
<button
|
||
onClick={() => setConfigOpenOAuth(o => !o)}
|
||
className="w-full flex items-center justify-between px-3 py-2.5 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800 bg-surface-secondary">
|
||
<span className="text-sm font-medium text-content-secondary">{t('settings.mcp.clientConfig')}</span>
|
||
{configOpenOAuth ? <ChevronDown className="w-4 h-4" style={{ color: 'var(--text-tertiary)' }} /> : <ChevronRight className="w-4 h-4" style={{ color: 'var(--text-tertiary)' }} />}
|
||
</button>
|
||
{configOpenOAuth && (
|
||
<div className="p-3 border-t border-edge">
|
||
<div className="flex justify-end mb-1.5">
|
||
<button onClick={() => handleCopy(mcpJsonConfigOAuth, 'json-oauth')}
|
||
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-xs border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border-edge text-content-secondary">
|
||
{copiedKey === 'json-oauth' ? <Check className="w-3 h-3 text-green-500" /> : <Copy className="w-3 h-3" />}
|
||
{copiedKey === 'json-oauth' ? t('settings.mcp.copied') : t('settings.mcp.copy')}
|
||
</button>
|
||
</div>
|
||
<pre className="p-3 rounded-lg text-xs font-mono overflow-x-auto border bg-surface-secondary border-edge text-content">
|
||
{mcpJsonConfigOAuth}
|
||
</pre>
|
||
<p className="mt-1.5 text-xs" style={{ color: 'var(--text-tertiary)' }}>{t('settings.mcp.clientConfigHintOAuth')}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs mb-3" style={{ color: 'var(--text-tertiary)' }}>{t('settings.oauth.clientsHint')}</p>
|
||
|
||
<div className="flex justify-end mb-2">
|
||
<button onClick={() => { setOauthCreateOpen(true); setOauthCreatedClient(null); setOauthNewName(''); setOauthNewUris(''); setOauthNewScopes([]); setOauthIsMachine(false) }}
|
||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors bg-slate-900 text-white hover:bg-slate-700">
|
||
<Plus className="w-3.5 h-3.5" /> {t('settings.oauth.createClient')}
|
||
</button>
|
||
</div>
|
||
|
||
{oauthClients.length === 0 ? (
|
||
<p className="text-sm py-3 text-center rounded-lg border border-edge" style={{ color: 'var(--text-tertiary)' }}>
|
||
{t('settings.oauth.noClients')}
|
||
</p>
|
||
) : (
|
||
<div className="rounded-lg border overflow-hidden border-edge">
|
||
{oauthClients.map((client, i) => (
|
||
<div key={client.id} className="px-4 py-3"
|
||
style={{ borderBottom: i < oauthClients.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||
<div className="flex items-center gap-3">
|
||
<KeyRound className="w-4 h-4 flex-shrink-0" style={{ color: 'var(--text-tertiary)' }} />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
<p className="text-sm font-medium truncate text-content">{client.name}</p>
|
||
{client.allows_client_credentials && (
|
||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0"
|
||
style={{ background: 'rgba(99,102,241,0.12)', color: '#4f46e5', border: '1px solid rgba(99,102,241,0.3)' }}>
|
||
{t('settings.oauth.badge.machine')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>
|
||
{t('settings.oauth.clientId')}: {client.client_id}
|
||
<span className="ml-3 font-sans">{t('settings.mcp.tokenCreatedAt')} {new Date(client.created_at).toLocaleDateString(locale)}</span>
|
||
</p>
|
||
<div className="flex flex-wrap gap-1 mt-1.5">
|
||
{(oauthScopesExpanded[client.id] ? client.allowed_scopes : client.allowed_scopes.slice(0, 5)).map(s => (
|
||
<span key={s} className="px-1.5 py-0.5 rounded text-xs border bg-surface-secondary border-edge" style={{ color: 'var(--text-tertiary)' }}>{s}</span>
|
||
))}
|
||
{client.allowed_scopes.length > 5 && (
|
||
<button
|
||
onClick={() => setOauthScopesExpanded(prev => ({ ...prev, [client.id]: !prev[client.id] }))}
|
||
className="px-1.5 py-0.5 rounded text-xs transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border border-edge"
|
||
style={{ color: 'var(--text-tertiary)' }}>
|
||
{oauthScopesExpanded[client.id] ? '−' : `+${client.allowed_scopes.length - 5}`}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<button onClick={() => setOauthRotateId(client.id)}
|
||
className="p-1.5 rounded-lg transition-colors hover:bg-amber-50 hover:text-amber-600 dark:hover:bg-amber-900/20"
|
||
style={{ color: 'var(--text-tertiary)' }} title={t('settings.oauth.rotateSecret')}>
|
||
<RefreshCw className="w-4 h-4" />
|
||
</button>
|
||
<button onClick={() => setOauthDeleteId(client.id)}
|
||
className="p-1.5 rounded-lg transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||
style={{ color: 'var(--text-tertiary)' }} title={t('settings.oauth.deleteClient')}>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Active OAuth Sessions */}
|
||
{oauthSessions.length > 0 && (
|
||
<div>
|
||
<label className="text-sm font-medium block mb-2 text-content-secondary">{t('settings.oauth.activeSessions')}</label>
|
||
<div className="rounded-lg border overflow-hidden border-edge">
|
||
{oauthSessions.map((session, i) => (
|
||
<div key={session.id} className="flex items-center gap-3 px-4 py-3"
|
||
style={{ borderBottom: i < oauthSessions.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium truncate text-content">{session.client_name}</p>
|
||
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>
|
||
{t('settings.oauth.sessionScopes')}: {session.scopes.join(', ')}
|
||
<span className="ml-3">{t('settings.oauth.sessionExpires')} {new Date(session.access_token_expires_at).toLocaleDateString(locale)}</span>
|
||
</p>
|
||
</div>
|
||
<button onClick={() => setOauthRevokeId(session.id)}
|
||
className="px-2.5 py-1 rounded text-xs border transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 border-edge"
|
||
style={{ color: 'var(--text-tertiary)' }}>
|
||
{t('settings.oauth.revoke')}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* API Tokens tab (deprecated) */}
|
||
{activeMcpTab === 'apitokens' && (
|
||
<>
|
||
<div className="flex items-baseline gap-2 px-3 py-2.5 rounded-lg" style={{ background: 'rgba(245,158,11,0.06)', border: '1px solid rgba(245,158,11,0.3)' }}>
|
||
<span className="text-amber-500 flex-shrink-0 leading-none">⚠</span>
|
||
<p className="text-xs" style={{ color: '#92400e' }}>{t('settings.mcp.apiTokensDeprecated')}</p>
|
||
</div>
|
||
|
||
{/* JSON config — API Token (collapsible) */}
|
||
<div className="rounded-lg border overflow-hidden border-edge">
|
||
<button
|
||
onClick={() => setConfigOpenToken(o => !o)}
|
||
className="w-full flex items-center justify-between px-3 py-2.5 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800 bg-surface-secondary">
|
||
<span className="text-sm font-medium text-content-secondary">{t('settings.mcp.clientConfig')}</span>
|
||
{configOpenToken ? <ChevronDown className="w-4 h-4" style={{ color: 'var(--text-tertiary)' }} /> : <ChevronRight className="w-4 h-4" style={{ color: 'var(--text-tertiary)' }} />}
|
||
</button>
|
||
{configOpenToken && (
|
||
<div className="p-3 border-t border-edge">
|
||
<div className="flex justify-end mb-1.5">
|
||
<button onClick={() => handleCopy(mcpJsonConfig, 'json-token')}
|
||
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-xs border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border-edge text-content-secondary">
|
||
{copiedKey === 'json-token' ? <Check className="w-3 h-3 text-green-500" /> : <Copy className="w-3 h-3" />}
|
||
{copiedKey === 'json-token' ? t('settings.mcp.copied') : t('settings.mcp.copy')}
|
||
</button>
|
||
</div>
|
||
<pre className="p-3 rounded-lg text-xs font-mono overflow-x-auto border bg-surface-secondary border-edge text-content">
|
||
{mcpJsonConfig}
|
||
</pre>
|
||
<p className="mt-1.5 text-xs" style={{ color: 'var(--text-tertiary)' }}>{t('settings.mcp.clientConfigHint')}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex justify-end">
|
||
<button onClick={() => { setMcpModalOpen(true); setMcpCreatedToken(null); setMcpNewName('') }}
|
||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors opacity-60 text-content-secondary"
|
||
style={{ background: 'var(--bg-tertiary, #e5e7eb)' }}>
|
||
<Plus className="w-3.5 h-3.5" /> {t('settings.mcp.createToken')}
|
||
</button>
|
||
</div>
|
||
|
||
{mcpTokens.length === 0 ? (
|
||
<p className="text-sm py-2 text-center" style={{ color: 'var(--text-tertiary)' }}>
|
||
{t('settings.mcp.noTokens')}
|
||
</p>
|
||
) : (
|
||
<div className="rounded-lg border overflow-hidden border-edge">
|
||
{mcpTokens.map((token, i) => (
|
||
<div key={token.id} className="flex items-center gap-3 px-4 py-3"
|
||
style={{ borderBottom: i < mcpTokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium truncate text-content">{token.name}</p>
|
||
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>
|
||
{token.token_prefix}...
|
||
<span className="ml-3 font-sans">{t('settings.mcp.tokenCreatedAt')} {new Date(token.created_at).toLocaleDateString(locale)}</span>
|
||
{token.last_used_at && (
|
||
<span className="ml-2">· {t('settings.mcp.tokenUsedAt')} {new Date(token.last_used_at).toLocaleDateString(locale)}</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
<button onClick={() => setMcpDeleteId(token.id)}
|
||
className="p-1.5 rounded-lg transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||
style={{ color: 'var(--text-tertiary)' }} title={t('settings.mcp.deleteTokenTitle')}>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</Section>
|
||
)
|
||
}
|
||
|
||
function McpTokenModals(props: any) {
|
||
const {
|
||
t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession,
|
||
} = props
|
||
return (
|
||
<>
|
||
{/* Create MCP Token modal */}
|
||
{mcpModalOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={e => { if (e.target === e.currentTarget && !mcpCreatedToken) setMcpModalOpen(false) }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4 bg-surface-card">
|
||
{!mcpCreatedToken ? (
|
||
<>
|
||
<h3 className="text-lg font-semibold text-content">{t('settings.mcp.modal.createTitle')}</h3>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.mcp.modal.tokenName')}</label>
|
||
<input type="text" value={mcpNewName} onChange={e => setMcpNewName(e.target.value)}
|
||
onKeyDown={e => e.key === 'Enter' && handleCreateMcpToken()}
|
||
placeholder={t('settings.mcp.modal.tokenNamePlaceholder')}
|
||
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content"
|
||
autoFocus />
|
||
</div>
|
||
<div className="flex gap-2 justify-end pt-1">
|
||
<button onClick={() => setMcpModalOpen(false)}
|
||
className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button onClick={handleCreateMcpToken} disabled={!mcpNewName.trim() || mcpCreating}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700 disabled:opacity-50">
|
||
{mcpCreating ? t('settings.mcp.modal.creating') : t('settings.mcp.modal.create')}
|
||
</button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<h3 className="text-lg font-semibold text-content">{t('settings.mcp.modal.createdTitle')}</h3>
|
||
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}>
|
||
<span className="text-amber-500 mt-0.5">⚠</span>
|
||
<p className="text-sm text-content-secondary">{t('settings.mcp.modal.createdWarning')}</p>
|
||
</div>
|
||
<div className="relative">
|
||
<pre className="p-3 pr-10 rounded-lg text-xs font-mono break-all border whitespace-pre-wrap bg-surface-secondary border-edge text-content">
|
||
{mcpCreatedToken}
|
||
</pre>
|
||
<button onClick={() => handleCopy(mcpCreatedToken, 'new-token')}
|
||
className="absolute top-2 right-2 p-1.5 rounded transition-colors hover:bg-slate-200 dark:hover:bg-slate-600 text-content-secondary"
|
||
title={t('settings.mcp.copy')}>
|
||
{copiedKey === 'new-token' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
<div className="flex justify-end">
|
||
<button onClick={() => { setMcpModalOpen(false); setMcpCreatedToken(null) }}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700">
|
||
{t('settings.mcp.modal.done')}
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Delete MCP Token confirm */}
|
||
{mcpDeleteId !== null && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={e => { if (e.target === e.currentTarget) setMcpDeleteId(null) }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
|
||
<h3 className="text-base font-semibold text-content">{t('settings.mcp.deleteTokenTitle')}</h3>
|
||
<p className="text-sm text-content-secondary">{t('settings.mcp.deleteTokenMessage')}</p>
|
||
<div className="flex gap-2 justify-end">
|
||
<button onClick={() => setMcpDeleteId(null)}
|
||
className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button onClick={() => handleDeleteMcpToken(mcpDeleteId)}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||
{t('settings.mcp.deleteTokenTitle')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</>
|
||
)
|
||
}
|
||
|
||
function OAuthClientModals(props: any) {
|
||
const {
|
||
t, locale, toast, mcpEnabled, oauthClients, setOauthClients, oauthSessions, setOauthSessions, oauthCreateOpen, setOauthCreateOpen, oauthNewName, setOauthNewName, oauthNewUris, setOauthNewUris, oauthNewScopes, setOauthNewScopes, oauthCreating, oauthCreatedClient, setOauthCreatedClient, oauthDeleteId, setOauthDeleteId, oauthRevokeId, setOauthRevokeId, oauthRotateId, setOauthRotateId, oauthRotatedSecret, setOauthRotatedSecret, oauthRotating, oauthScopesExpanded, setOauthScopesExpanded, oauthIsMachine, setOauthIsMachine, activeMcpTab, setActiveMcpTab, configOpenOAuth, setConfigOpenOAuth, configOpenToken, setConfigOpenToken, mcpTokens, setMcpTokens, mcpModalOpen, setMcpModalOpen, mcpNewName, setMcpNewName, mcpCreatedToken, setMcpCreatedToken, mcpCreating, mcpDeleteId, setMcpDeleteId, copiedKey, mcpEndpoint, mcpJsonConfigOAuth, mcpJsonConfig, handleCreateMcpToken, handleDeleteMcpToken, handleCopy, handleCreateOAuthClient, handleDeleteOAuthClient, handleRotateSecret, handleRevokeSession,
|
||
} = props
|
||
return (
|
||
<>
|
||
{/* Create OAuth Client modal */}
|
||
{oauthCreateOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={e => { if (e.target === e.currentTarget && !oauthCreatedClient) setOauthCreateOpen(false) }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-lg p-6 space-y-4 overflow-y-auto max-h-[90vh] bg-surface-card">
|
||
{!oauthCreatedClient ? (
|
||
<>
|
||
<h3 className="text-lg font-semibold text-content">{t('settings.oauth.modal.createTitle')}</h3>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium mb-2" style={{ color: 'var(--text-tertiary)' }}>{t('settings.oauth.modal.presets')}</label>
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{OAUTH_PRESETS.map(preset => (
|
||
<button
|
||
key={preset.id}
|
||
type="button"
|
||
onClick={() => {
|
||
setOauthNewName(preset.name)
|
||
setOauthNewUris(preset.uris)
|
||
setOauthNewScopes(preset.scopes)
|
||
}}
|
||
className="px-2.5 py-1 rounded-md text-xs font-medium border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border-edge text-content-secondary bg-surface-secondary">
|
||
{preset.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.oauth.modal.clientName')}</label>
|
||
<input type="text" value={oauthNewName} onChange={e => setOauthNewName(e.target.value)}
|
||
placeholder={t('settings.oauth.modal.clientNamePlaceholder')}
|
||
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content"
|
||
autoFocus />
|
||
</div>
|
||
|
||
<label className="flex items-start gap-2.5 cursor-pointer">
|
||
<input type="checkbox" checked={oauthIsMachine} onChange={e => setOauthIsMachine(e.target.checked)}
|
||
className="mt-0.5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />
|
||
<div>
|
||
<span className="text-sm font-medium text-content-secondary">{t('settings.oauth.modal.machineClient')}</span>
|
||
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{t('settings.oauth.modal.machineClientHint')}</p>
|
||
</div>
|
||
</label>
|
||
|
||
{!oauthIsMachine && (
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1.5 text-content-secondary">{t('settings.oauth.modal.redirectUris')}</label>
|
||
<textarea value={oauthNewUris} onChange={e => setOauthNewUris(e.target.value)}
|
||
placeholder={t('settings.oauth.modal.redirectUrisPlaceholder')}
|
||
rows={3}
|
||
className="w-full px-3 py-2.5 border rounded-lg text-sm font-mono resize-none focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content" />
|
||
<p className="mt-1 text-xs" style={{ color: 'var(--text-tertiary)' }}>{t('settings.oauth.modal.redirectUrisHint')}</p>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1 text-content-secondary">{t('settings.oauth.modal.scopes')}</label>
|
||
<p className="text-xs mb-2" style={{ color: 'var(--text-tertiary)' }}>{t('settings.oauth.modal.scopesHint')}</p>
|
||
<ScopeGroupPicker selected={oauthNewScopes} onChange={setOauthNewScopes} />
|
||
</div>
|
||
|
||
<div className="flex gap-2 justify-end pt-1">
|
||
<button onClick={() => setOauthCreateOpen(false)}
|
||
className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button onClick={handleCreateOAuthClient}
|
||
disabled={!oauthNewName.trim() || (!oauthIsMachine && !oauthNewUris.trim()) || oauthCreating}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700 disabled:opacity-50">
|
||
{oauthCreating ? t('settings.oauth.modal.creating') : t('settings.oauth.modal.create')}
|
||
</button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<h3 className="text-lg font-semibold text-content">{t('settings.oauth.modal.createdTitle')}</h3>
|
||
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}>
|
||
<span className="text-amber-500 mt-0.5">⚠</span>
|
||
<p className="text-sm text-content-secondary">{t('settings.oauth.modal.createdWarning')}</p>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div>
|
||
<label className="block text-xs font-medium mb-1 text-content-secondary">{t('settings.oauth.clientId')}</label>
|
||
<div className="flex items-center gap-2">
|
||
<code className="flex-1 px-3 py-2 rounded-lg text-xs font-mono border bg-surface-secondary border-edge text-content">
|
||
{oauthCreatedClient.client_id}
|
||
</code>
|
||
<button onClick={() => handleCopy(oauthCreatedClient.client_id, 'new-client-id')}
|
||
className="p-2 rounded-lg border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border-edge">
|
||
{copiedKey === 'new-client-id' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4 text-content-secondary" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-medium mb-1 text-content-secondary">{t('settings.oauth.clientSecret')}</label>
|
||
<div className="flex items-center gap-2">
|
||
<code className="flex-1 px-3 py-2 rounded-lg text-xs font-mono border break-all bg-surface-secondary border-edge text-content">
|
||
{oauthCreatedClient.client_secret}
|
||
</code>
|
||
<button onClick={() => handleCopy(oauthCreatedClient.client_secret!, 'new-client-secret')}
|
||
className="p-2 rounded-lg border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border-edge">
|
||
{copiedKey === 'new-client-secret' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4 text-content-secondary" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{oauthCreatedClient?.allows_client_credentials && (
|
||
<div className="p-3 rounded-lg border text-xs font-mono bg-surface-secondary border-edge" style={{ color: 'var(--text-tertiary)' }}>
|
||
{t('settings.oauth.modal.machineClientUsage')}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex justify-end">
|
||
<button onClick={() => { setOauthCreateOpen(false); setOauthCreatedClient(null) }}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700">
|
||
{t('settings.mcp.modal.done')}
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Delete OAuth Client confirm */}
|
||
{oauthDeleteId !== null && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={e => { if (e.target === e.currentTarget) setOauthDeleteId(null) }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
|
||
<h3 className="text-base font-semibold text-content">{t('settings.oauth.deleteClient')}</h3>
|
||
<p className="text-sm text-content-secondary">{t('settings.oauth.deleteClientMessage')}</p>
|
||
<div className="flex gap-2 justify-end">
|
||
<button onClick={() => setOauthDeleteId(null)}
|
||
className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button onClick={() => handleDeleteOAuthClient(oauthDeleteId)}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||
{t('settings.oauth.deleteClient')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Rotate OAuth Client Secret confirm */}
|
||
{oauthRotateId !== null && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={e => { if (e.target === e.currentTarget) setOauthRotateId(null) }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
|
||
<h3 className="text-base font-semibold text-content">{t('settings.oauth.rotateSecret')}</h3>
|
||
<p className="text-sm text-content-secondary">{t('settings.oauth.rotateSecretMessage')}</p>
|
||
<div className="flex gap-2 justify-end">
|
||
<button onClick={() => setOauthRotateId(null)}
|
||
className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button onClick={() => handleRotateSecret(oauthRotateId)} disabled={oauthRotating}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700 disabled:opacity-50">
|
||
{oauthRotating ? t('settings.oauth.rotateSecretConfirming') : t('settings.oauth.rotateSecretConfirm')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Rotated Secret display */}
|
||
{oauthRotatedSecret !== null && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4 bg-surface-card">
|
||
<h3 className="text-lg font-semibold text-content">{t('settings.oauth.rotateSecretDoneTitle')}</h3>
|
||
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}>
|
||
<span className="text-amber-500 mt-0.5">⚠</span>
|
||
<p className="text-sm text-content-secondary">{t('settings.oauth.rotateSecretDoneWarning')}</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-medium mb-1 text-content-secondary">{t('settings.oauth.clientSecret')}</label>
|
||
<div className="flex items-center gap-2">
|
||
<code className="flex-1 px-3 py-2 rounded-lg text-xs font-mono border break-all bg-surface-secondary border-edge text-content">
|
||
{oauthRotatedSecret}
|
||
</code>
|
||
<button onClick={() => handleCopy(oauthRotatedSecret, 'rotated-secret')}
|
||
className="p-2 rounded-lg border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 border-edge">
|
||
{copiedKey === 'rotated-secret' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4 text-content-secondary" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-end">
|
||
<button onClick={() => setOauthRotatedSecret(null)}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-slate-900 hover:bg-slate-700">
|
||
{t('settings.mcp.modal.done')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Revoke OAuth Session confirm */}
|
||
{oauthRevokeId !== null && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={e => { if (e.target === e.currentTarget) setOauthRevokeId(null) }}>
|
||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
|
||
<h3 className="text-base font-semibold text-content">{t('settings.oauth.revokeSession')}</h3>
|
||
<p className="text-sm text-content-secondary">{t('settings.oauth.revokeSessionMessage')}</p>
|
||
<div className="flex gap-2 justify-end">
|
||
<button onClick={() => setOauthRevokeId(null)}
|
||
className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button onClick={() => handleRevokeSession(oauthRevokeId)}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||
{t('settings.oauth.revoke')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
}
|