mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(admin): add OAuth sessions to MCP Access panel
Show active OAuth sessions (first) and static API tokens (second) in the admin MCP Access tab. Admins can revoke any OAuth session, which immediately terminates the live MCP transport for that client. - Add admin-level listOAuthSessions / revokeOAuthSession in adminService - Add GET /admin/oauth-sessions and DELETE /admin/oauth-sessions/:id routes - Restructure AdminMcpTokensPanel into two sections; rename tab to MCP Access - Fix stale writeAudit call in rotate-jwt-secret route (user_id → userId) - Add admin.oauthSessions.* i18n keys across all 14 locale files
This commit is contained in:
@@ -232,6 +232,8 @@ export const adminApi = {
|
||||
apiClient.get('/admin/audit-log', { params }).then(r => r.data),
|
||||
mcpTokens: () => apiClient.get('/admin/mcp-tokens').then(r => r.data),
|
||||
deleteMcpToken: (id: number) => apiClient.delete(`/admin/mcp-tokens/${id}`).then(r => r.data),
|
||||
oauthSessions: () => apiClient.get('/admin/oauth-sessions').then(r => r.data),
|
||||
revokeOAuthSession: (id: number) => apiClient.delete(`/admin/oauth-sessions/${id}`).then(r => r.data),
|
||||
getPermissions: () => apiClient.get('/admin/permissions').then(r => r.data),
|
||||
updatePermissions: (permissions: Record<string, string>) => apiClient.put('/admin/permissions', { permissions }).then(r => r.data),
|
||||
rotateJwtSecret: () => apiClient.post('/admin/rotate-jwt-secret').then(r => r.data),
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { adminApi } from '../../api/client'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { Key, Trash2, User, Loader2 } from 'lucide-react'
|
||||
import { Key, Trash2, User, Loader2, Shield } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
interface AdminOAuthSession {
|
||||
id: number
|
||||
client_id: string
|
||||
client_name: string
|
||||
user_id: number
|
||||
username: string
|
||||
scopes: string[]
|
||||
access_token_expires_at: string
|
||||
refresh_token_expires_at: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface AdminMcpToken {
|
||||
id: number
|
||||
name: string
|
||||
@@ -15,20 +27,38 @@ interface AdminMcpToken {
|
||||
}
|
||||
|
||||
export default function AdminMcpTokensPanel() {
|
||||
const [sessions, setSessions] = useState<AdminOAuthSession[]>([])
|
||||
const [sessionsLoading, setSessionsLoading] = useState(true)
|
||||
const [tokens, setTokens] = useState<AdminMcpToken[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [tokensLoading, setTokensLoading] = useState(true)
|
||||
const [revokeConfirmId, setRevokeConfirmId] = useState<number | null>(null)
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null)
|
||||
const toast = useToast()
|
||||
const { t, locale } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
adminApi.oauthSessions()
|
||||
.then(d => setSessions(d.sessions || []))
|
||||
.catch(() => toast.error(t('admin.oauthSessions.loadError')))
|
||||
.finally(() => setSessionsLoading(false))
|
||||
|
||||
adminApi.mcpTokens()
|
||||
.then(d => setTokens(d.tokens || []))
|
||||
.catch(() => toast.error(t('admin.mcpTokens.loadError')))
|
||||
.finally(() => setIsLoading(false))
|
||||
.finally(() => setTokensLoading(false))
|
||||
}, [])
|
||||
|
||||
const handleRevoke = async (id: number) => {
|
||||
try {
|
||||
await adminApi.revokeOAuthSession(id)
|
||||
setSessions(prev => prev.filter(s => s.id !== id))
|
||||
setRevokeConfirmId(null)
|
||||
toast.success(t('admin.oauthSessions.revokeSuccess'))
|
||||
} catch {
|
||||
toast.error(t('admin.oauthSessions.revokeError'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await adminApi.deleteMcpToken(id)
|
||||
@@ -47,55 +77,134 @@ export default function AdminMcpTokensPanel() {
|
||||
<p className="text-sm mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
) : tokens.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2">
|
||||
<Key className="w-8 h-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b"
|
||||
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}>
|
||||
<span>{t('admin.mcpTokens.tokenName')}</span>
|
||||
<span>{t('admin.mcpTokens.owner')}</span>
|
||||
<span className="text-right">{t('admin.mcpTokens.created')}</span>
|
||||
<span className="text-right">{t('admin.mcpTokens.lastUsed')}</span>
|
||||
<span></span>
|
||||
{/* OAuth Sessions */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{t('admin.oauthSessions.sectionTitle')}</h3>
|
||||
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}>
|
||||
{sessionsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
{tokens.map((token, i) => (
|
||||
<div key={token.id}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3"
|
||||
style={{ borderBottom: i < tokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{token.name}</p>
|
||||
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{token.token_prefix}...</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<User className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">{token.username}</span>
|
||||
</div>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{new Date(token.created_at).toLocaleDateString(locale)}
|
||||
</span>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{token.last_used_at ? new Date(token.last_used_at).toLocaleDateString(locale) : t('admin.mcpTokens.never')}
|
||||
</span>
|
||||
<button onClick={() => setDeleteConfirmId(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('common.delete')}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2">
|
||||
<Shield className="w-8 h-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>{t('admin.oauthSessions.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b"
|
||||
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}>
|
||||
<span>{t('admin.oauthSessions.clientName')}</span>
|
||||
<span>{t('admin.oauthSessions.owner')}</span>
|
||||
<span>{t('admin.oauthSessions.scopes')}</span>
|
||||
<span className="text-right">{t('admin.oauthSessions.created')}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{sessions.map((session, i) => (
|
||||
<div key={session.id}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3"
|
||||
style={{ borderBottom: i < sessions.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{session.client_name}</p>
|
||||
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{session.client_id}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<User className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">{session.username}</span>
|
||||
</div>
|
||||
<span className="text-xs whitespace-nowrap" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{session.scopes.join(', ')}
|
||||
</span>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{new Date(session.created_at).toLocaleDateString(locale)}
|
||||
</span>
|
||||
<button onClick={() => setRevokeConfirmId(session.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('common.delete')}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MCP Tokens */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{t('admin.mcpTokens.sectionTitle')}</h3>
|
||||
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}>
|
||||
{tokensLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
) : tokens.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2">
|
||||
<Key className="w-8 h-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b"
|
||||
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}>
|
||||
<span>{t('admin.mcpTokens.tokenName')}</span>
|
||||
<span>{t('admin.mcpTokens.owner')}</span>
|
||||
<span className="text-right">{t('admin.mcpTokens.created')}</span>
|
||||
<span className="text-right">{t('admin.mcpTokens.lastUsed')}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{tokens.map((token, i) => (
|
||||
<div key={token.id}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3"
|
||||
style={{ borderBottom: i < tokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{token.name}</p>
|
||||
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{token.token_prefix}...</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<User className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">{token.username}</span>
|
||||
</div>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{new Date(token.created_at).toLocaleDateString(locale)}
|
||||
</span>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{token.last_used_at ? new Date(token.last_used_at).toLocaleDateString(locale) : t('admin.mcpTokens.never')}
|
||||
</span>
|
||||
<button onClick={() => setDeleteConfirmId(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('common.delete')}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revoke OAuth session modal */}
|
||||
{revokeConfirmId !== 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) setRevokeConfirmId(null) }}>
|
||||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.oauthSessions.revokeTitle')}</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('admin.oauthSessions.revokeMessage')}</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button onClick={() => setRevokeConfirmId(null)}
|
||||
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button onClick={() => handleRevoke(revokeConfirmId)}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||||
{t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete MCP token modal */}
|
||||
{deleteConfirmId !== 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) setDeleteConfirmId(null) }}>
|
||||
|
||||
@@ -411,9 +411,10 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.tabs.config': 'التخصيص',
|
||||
'admin.tabs.templates': 'قوالب التعبئة',
|
||||
'admin.tabs.addons': 'الإضافات',
|
||||
'admin.tabs.mcpTokens': 'رموز MCP',
|
||||
'admin.mcpTokens.title': 'رموز MCP',
|
||||
'admin.mcpTokens.subtitle': 'إدارة رموز API لجميع المستخدمين',
|
||||
'admin.tabs.mcpTokens': 'وصول MCP',
|
||||
'admin.mcpTokens.title': 'وصول MCP',
|
||||
'admin.mcpTokens.subtitle': 'إدارة جلسات OAuth ورموز API لجميع المستخدمين',
|
||||
'admin.mcpTokens.sectionTitle': 'رموز API',
|
||||
'admin.mcpTokens.owner': 'المالك',
|
||||
'admin.mcpTokens.tokenName': 'اسم الرمز',
|
||||
'admin.mcpTokens.created': 'تاريخ الإنشاء',
|
||||
@@ -425,6 +426,17 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'تم حذف الرمز',
|
||||
'admin.mcpTokens.deleteError': 'فشل حذف الرمز',
|
||||
'admin.mcpTokens.loadError': 'فشل تحميل الرموز',
|
||||
'admin.oauthSessions.sectionTitle': 'جلسات OAuth',
|
||||
'admin.oauthSessions.clientName': 'العميل',
|
||||
'admin.oauthSessions.owner': 'المالك',
|
||||
'admin.oauthSessions.scopes': 'الصلاحيات',
|
||||
'admin.oauthSessions.created': 'تاريخ الإنشاء',
|
||||
'admin.oauthSessions.empty': 'لا توجد جلسات OAuth نشطة',
|
||||
'admin.oauthSessions.revokeTitle': 'إلغاء الجلسة',
|
||||
'admin.oauthSessions.revokeMessage': 'سيتم إلغاء جلسة OAuth هذه فوراً. سيفقد العميل وصوله إلى MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'تم إلغاء الجلسة',
|
||||
'admin.oauthSessions.revokeError': 'فشل إلغاء الجلسة',
|
||||
'admin.oauthSessions.loadError': 'فشل تحميل جلسات OAuth',
|
||||
'admin.tabs.github': 'GitHub',
|
||||
'admin.stats.users': 'المستخدمون',
|
||||
'admin.stats.trips': 'الرحلات',
|
||||
|
||||
@@ -1478,9 +1478,10 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
|
||||
// Permissions
|
||||
'admin.tabs.permissions': 'Permissões',
|
||||
'admin.tabs.mcpTokens': 'Tokens MCP',
|
||||
'admin.mcpTokens.title': 'Tokens MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gerenciar tokens de API de todos os usuários',
|
||||
'admin.tabs.mcpTokens': 'Acesso MCP',
|
||||
'admin.mcpTokens.title': 'Acesso MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gerenciar sessões OAuth e tokens de API de todos os usuários',
|
||||
'admin.mcpTokens.sectionTitle': 'Tokens de API',
|
||||
'admin.mcpTokens.owner': 'Proprietário',
|
||||
'admin.mcpTokens.tokenName': 'Nome do Token',
|
||||
'admin.mcpTokens.created': 'Criado',
|
||||
@@ -1492,6 +1493,17 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token excluído',
|
||||
'admin.mcpTokens.deleteError': 'Falha ao excluir token',
|
||||
'admin.mcpTokens.loadError': 'Falha ao carregar tokens',
|
||||
'admin.oauthSessions.sectionTitle': 'Sessões OAuth',
|
||||
'admin.oauthSessions.clientName': 'Cliente',
|
||||
'admin.oauthSessions.owner': 'Proprietário',
|
||||
'admin.oauthSessions.scopes': 'Permissões',
|
||||
'admin.oauthSessions.created': 'Criado',
|
||||
'admin.oauthSessions.empty': 'Nenhuma sessão OAuth ativa',
|
||||
'admin.oauthSessions.revokeTitle': 'Revogar sessão',
|
||||
'admin.oauthSessions.revokeMessage': 'Esta sessão OAuth será revogada imediatamente. O cliente perderá o acesso MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sessão revogada',
|
||||
'admin.oauthSessions.revokeError': 'Falha ao revogar sessão',
|
||||
'admin.oauthSessions.loadError': 'Falha ao carregar sessões OAuth',
|
||||
'perm.title': 'Configurações de Permissões',
|
||||
'perm.subtitle': 'Controle quem pode realizar ações no aplicativo',
|
||||
'perm.saved': 'Configurações de permissões salvas',
|
||||
|
||||
@@ -551,9 +551,10 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.audit.col.details': 'Detaily',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP tokeny',
|
||||
'admin.mcpTokens.title': 'MCP tokeny',
|
||||
'admin.mcpTokens.subtitle': 'Správa API tokenů všech uživatelů',
|
||||
'admin.tabs.mcpTokens': 'MCP přístup',
|
||||
'admin.mcpTokens.title': 'MCP přístup',
|
||||
'admin.mcpTokens.subtitle': 'Správa OAuth relací a API tokenů všech uživatelů',
|
||||
'admin.mcpTokens.sectionTitle': 'API tokeny',
|
||||
'admin.mcpTokens.owner': 'Vlastník',
|
||||
'admin.mcpTokens.tokenName': 'Název tokenu',
|
||||
'admin.mcpTokens.created': 'Vytvořen',
|
||||
@@ -565,6 +566,17 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token smazán',
|
||||
'admin.mcpTokens.deleteError': 'Nepodařilo se smazat token',
|
||||
'admin.mcpTokens.loadError': 'Nepodařilo se načíst tokeny',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth relace',
|
||||
'admin.oauthSessions.clientName': 'Klient',
|
||||
'admin.oauthSessions.owner': 'Vlastník',
|
||||
'admin.oauthSessions.scopes': 'Oprávnění',
|
||||
'admin.oauthSessions.created': 'Vytvořeno',
|
||||
'admin.oauthSessions.empty': 'Žádné aktivní OAuth relace',
|
||||
'admin.oauthSessions.revokeTitle': 'Zrušit relaci',
|
||||
'admin.oauthSessions.revokeMessage': 'Tato OAuth relace bude okamžitě zrušena. Klient ztratí přístup k MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Relace zrušena',
|
||||
'admin.oauthSessions.revokeError': 'Nepodařilo se zrušit relaci',
|
||||
'admin.oauthSessions.loadError': 'Nepodařilo se načíst OAuth relace',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -549,9 +549,10 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.weather.locationHint': 'Das Wetter wird anhand des ersten Ortes mit Koordinaten im jeweiligen Tag berechnet. Ist kein Ort am Tag eingeplant, wird ein beliebiger Ort aus der Ortsliste als Referenz verwendet.',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP-Tokens',
|
||||
'admin.mcpTokens.title': 'MCP-Tokens',
|
||||
'admin.mcpTokens.subtitle': 'API-Tokens aller Benutzer verwalten',
|
||||
'admin.tabs.mcpTokens': 'MCP-Zugang',
|
||||
'admin.mcpTokens.title': 'MCP-Zugang',
|
||||
'admin.mcpTokens.subtitle': 'OAuth-Sitzungen und API-Tokens aller Benutzer verwalten',
|
||||
'admin.mcpTokens.sectionTitle': 'API-Tokens',
|
||||
'admin.mcpTokens.owner': 'Besitzer',
|
||||
'admin.mcpTokens.tokenName': 'Token-Name',
|
||||
'admin.mcpTokens.created': 'Erstellt',
|
||||
@@ -563,6 +564,17 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token gelöscht',
|
||||
'admin.mcpTokens.deleteError': 'Token konnte nicht gelöscht werden',
|
||||
'admin.mcpTokens.loadError': 'Tokens konnten nicht geladen werden',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth-Sitzungen',
|
||||
'admin.oauthSessions.clientName': 'Client',
|
||||
'admin.oauthSessions.owner': 'Besitzer',
|
||||
'admin.oauthSessions.scopes': 'Berechtigungen',
|
||||
'admin.oauthSessions.created': 'Erstellt',
|
||||
'admin.oauthSessions.empty': 'Keine aktiven OAuth-Sitzungen',
|
||||
'admin.oauthSessions.revokeTitle': 'Sitzung widerrufen',
|
||||
'admin.oauthSessions.revokeMessage': 'Diese OAuth-Sitzung wird sofort widerrufen. Der Client verliert den MCP-Zugang.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sitzung widerrufen',
|
||||
'admin.oauthSessions.revokeError': 'Sitzung konnte nicht widerrufen werden',
|
||||
'admin.oauthSessions.loadError': 'OAuth-Sitzungen konnten nicht geladen werden',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -613,9 +613,10 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.weather.locationHint': 'Weather is based on the first place with coordinates in each day. If no place is assigned to a day, any place from the place list is used as a reference.',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.mcpTokens': 'MCP Tokens',
|
||||
'admin.mcpTokens.title': 'MCP Tokens',
|
||||
'admin.mcpTokens.subtitle': 'Manage API tokens across all users',
|
||||
'admin.tabs.mcpTokens': 'MCP Access',
|
||||
'admin.mcpTokens.title': 'MCP Access',
|
||||
'admin.mcpTokens.subtitle': 'Manage OAuth sessions and API tokens across all users',
|
||||
'admin.mcpTokens.sectionTitle': 'API Tokens',
|
||||
'admin.mcpTokens.owner': 'Owner',
|
||||
'admin.mcpTokens.tokenName': 'Token Name',
|
||||
'admin.mcpTokens.created': 'Created',
|
||||
@@ -627,6 +628,17 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token deleted',
|
||||
'admin.mcpTokens.deleteError': 'Failed to delete token',
|
||||
'admin.mcpTokens.loadError': 'Failed to load tokens',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth Sessions',
|
||||
'admin.oauthSessions.clientName': 'Client',
|
||||
'admin.oauthSessions.owner': 'Owner',
|
||||
'admin.oauthSessions.scopes': 'Scopes',
|
||||
'admin.oauthSessions.created': 'Created',
|
||||
'admin.oauthSessions.empty': 'No active OAuth sessions',
|
||||
'admin.oauthSessions.revokeTitle': 'Revoke Session',
|
||||
'admin.oauthSessions.revokeMessage': 'This will revoke the OAuth session immediately. The client will lose MCP access.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Session revoked',
|
||||
'admin.oauthSessions.revokeError': 'Failed to revoke session',
|
||||
'admin.oauthSessions.loadError': 'Failed to load OAuth sessions',
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
'admin.audit.subtitle': 'Security-sensitive and administration events (backups, users, MFA, settings).',
|
||||
|
||||
@@ -526,9 +526,10 @@ const es: Record<string, string> = {
|
||||
'admin.weather.locationHint': 'El tiempo se basa en el primer lugar con coordenadas de cada día. Si no hay ningún lugar asignado a un día, se usa como referencia cualquier lugar de la lista.',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'Tokens MCP',
|
||||
'admin.mcpTokens.title': 'Tokens MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gestionar tokens de API de todos los usuarios',
|
||||
'admin.tabs.mcpTokens': 'Acceso MCP',
|
||||
'admin.mcpTokens.title': 'Acceso MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gestionar sesiones OAuth y tokens de API de todos los usuarios',
|
||||
'admin.mcpTokens.sectionTitle': 'Tokens de API',
|
||||
'admin.mcpTokens.owner': 'Propietario',
|
||||
'admin.mcpTokens.tokenName': 'Nombre del token',
|
||||
'admin.mcpTokens.created': 'Creado',
|
||||
@@ -540,6 +541,17 @@ const es: Record<string, string> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token eliminado',
|
||||
'admin.mcpTokens.deleteError': 'No se pudo eliminar el token',
|
||||
'admin.mcpTokens.loadError': 'No se pudieron cargar los tokens',
|
||||
'admin.oauthSessions.sectionTitle': 'Sesiones OAuth',
|
||||
'admin.oauthSessions.clientName': 'Cliente',
|
||||
'admin.oauthSessions.owner': 'Propietario',
|
||||
'admin.oauthSessions.scopes': 'Permisos',
|
||||
'admin.oauthSessions.created': 'Creado',
|
||||
'admin.oauthSessions.empty': 'No hay sesiones OAuth activas',
|
||||
'admin.oauthSessions.revokeTitle': 'Revocar sesión',
|
||||
'admin.oauthSessions.revokeMessage': 'Esto revocará la sesión OAuth inmediatamente. El cliente perderá el acceso MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sesión revocada',
|
||||
'admin.oauthSessions.revokeError': 'No se pudo revocar la sesión',
|
||||
'admin.oauthSessions.loadError': 'No se pudieron cargar las sesiones OAuth',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -561,9 +561,10 @@ const fr: Record<string, string> = {
|
||||
'admin.audit.col.details': 'Détails',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'Tokens MCP',
|
||||
'admin.mcpTokens.title': 'Tokens MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gérer les tokens API de tous les utilisateurs',
|
||||
'admin.tabs.mcpTokens': 'Accès MCP',
|
||||
'admin.mcpTokens.title': 'Accès MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gérer les sessions OAuth et les tokens API de tous les utilisateurs',
|
||||
'admin.mcpTokens.sectionTitle': 'Tokens API',
|
||||
'admin.mcpTokens.owner': 'Propriétaire',
|
||||
'admin.mcpTokens.tokenName': 'Nom du token',
|
||||
'admin.mcpTokens.created': 'Créé',
|
||||
@@ -575,6 +576,17 @@ const fr: Record<string, string> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token supprimé',
|
||||
'admin.mcpTokens.deleteError': 'Impossible de supprimer le token',
|
||||
'admin.mcpTokens.loadError': 'Impossible de charger les tokens',
|
||||
'admin.oauthSessions.sectionTitle': 'Sessions OAuth',
|
||||
'admin.oauthSessions.clientName': 'Client',
|
||||
'admin.oauthSessions.owner': 'Propriétaire',
|
||||
'admin.oauthSessions.scopes': 'Portées',
|
||||
'admin.oauthSessions.created': 'Créé',
|
||||
'admin.oauthSessions.empty': 'Aucune session OAuth active',
|
||||
'admin.oauthSessions.revokeTitle': 'Révoquer la session',
|
||||
'admin.oauthSessions.revokeMessage': 'Cette session OAuth sera révoquée immédiatement. Le client perdra l\'accès MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Session révoquée',
|
||||
'admin.oauthSessions.revokeError': 'Impossible de révoquer la session',
|
||||
'admin.oauthSessions.loadError': 'Impossible de charger les sessions OAuth',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -562,9 +562,10 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.audit.col.details': 'Részletek',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP tokenek',
|
||||
'admin.mcpTokens.title': 'MCP tokenek',
|
||||
'admin.mcpTokens.subtitle': 'Összes felhasználó API tokeneinek kezelése',
|
||||
'admin.tabs.mcpTokens': 'MCP hozzáférés',
|
||||
'admin.mcpTokens.title': 'MCP hozzáférés',
|
||||
'admin.mcpTokens.subtitle': 'OAuth munkamenetek és API tokenek kezelése az összes felhasználó számára',
|
||||
'admin.mcpTokens.sectionTitle': 'API tokenek',
|
||||
'admin.mcpTokens.owner': 'Tulajdonos',
|
||||
'admin.mcpTokens.tokenName': 'Token neve',
|
||||
'admin.mcpTokens.created': 'Létrehozva',
|
||||
@@ -576,6 +577,17 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token törölve',
|
||||
'admin.mcpTokens.deleteError': 'Nem sikerült törölni a tokent',
|
||||
'admin.mcpTokens.loadError': 'Nem sikerült betölteni a tokeneket',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth munkamenetek',
|
||||
'admin.oauthSessions.clientName': 'Kliens',
|
||||
'admin.oauthSessions.owner': 'Tulajdonos',
|
||||
'admin.oauthSessions.scopes': 'Jogosultságok',
|
||||
'admin.oauthSessions.created': 'Létrehozva',
|
||||
'admin.oauthSessions.empty': 'Nincsenek aktív OAuth munkamenetek',
|
||||
'admin.oauthSessions.revokeTitle': 'Munkamenet visszavonása',
|
||||
'admin.oauthSessions.revokeMessage': 'Ez az OAuth munkamenet azonnal visszavonásra kerül. A kliens elveszíti az MCP hozzáférést.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Munkamenet visszavonva',
|
||||
'admin.oauthSessions.revokeError': 'Nem sikerült visszavonni a munkamenetet',
|
||||
'admin.oauthSessions.loadError': 'Nem sikerült betölteni az OAuth munkameneteket',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -562,9 +562,10 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.audit.col.details': 'Dettagli',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'Token MCP',
|
||||
'admin.mcpTokens.title': 'Token MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gestisci i token API di tutti gli utenti',
|
||||
'admin.tabs.mcpTokens': 'Accesso MCP',
|
||||
'admin.mcpTokens.title': 'Accesso MCP',
|
||||
'admin.mcpTokens.subtitle': 'Gestisci le sessioni OAuth e i token API di tutti gli utenti',
|
||||
'admin.mcpTokens.sectionTitle': 'Token API',
|
||||
'admin.mcpTokens.owner': 'Proprietario',
|
||||
'admin.mcpTokens.tokenName': 'Nome token',
|
||||
'admin.mcpTokens.created': 'Creato',
|
||||
@@ -576,6 +577,17 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token eliminato',
|
||||
'admin.mcpTokens.deleteError': 'Impossibile eliminare il token',
|
||||
'admin.mcpTokens.loadError': 'Impossibile caricare i token',
|
||||
'admin.oauthSessions.sectionTitle': 'Sessioni OAuth',
|
||||
'admin.oauthSessions.clientName': 'Client',
|
||||
'admin.oauthSessions.owner': 'Proprietario',
|
||||
'admin.oauthSessions.scopes': 'Ambiti',
|
||||
'admin.oauthSessions.created': 'Creato',
|
||||
'admin.oauthSessions.empty': 'Nessuna sessione OAuth attiva',
|
||||
'admin.oauthSessions.revokeTitle': 'Revoca sessione',
|
||||
'admin.oauthSessions.revokeMessage': 'Questa sessione OAuth verrà revocata immediatamente. Il client perderà l\'accesso MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sessione revocata',
|
||||
'admin.oauthSessions.revokeError': 'Impossibile revocare la sessione',
|
||||
'admin.oauthSessions.loadError': 'Impossibile caricare le sessioni OAuth',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -548,9 +548,10 @@ const nl: Record<string, string> = {
|
||||
'admin.weather.locationHint': 'Het weer is gebaseerd op de eerste plaats met coördinaten op elke dag. Als er geen plaats aan een dag is toegewezen, wordt een plaats uit de lijst als referentie gebruikt.',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP-tokens',
|
||||
'admin.mcpTokens.title': 'MCP-tokens',
|
||||
'admin.mcpTokens.subtitle': 'API-tokens van alle gebruikers beheren',
|
||||
'admin.tabs.mcpTokens': 'MCP-toegang',
|
||||
'admin.mcpTokens.title': 'MCP-toegang',
|
||||
'admin.mcpTokens.subtitle': 'OAuth-sessies en API-tokens van alle gebruikers beheren',
|
||||
'admin.mcpTokens.sectionTitle': 'API-tokens',
|
||||
'admin.mcpTokens.owner': 'Eigenaar',
|
||||
'admin.mcpTokens.tokenName': 'Tokennaam',
|
||||
'admin.mcpTokens.created': 'Aangemaakt',
|
||||
@@ -562,6 +563,17 @@ const nl: Record<string, string> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token verwijderd',
|
||||
'admin.mcpTokens.deleteError': 'Token kon niet worden verwijderd',
|
||||
'admin.mcpTokens.loadError': 'Tokens konden niet worden geladen',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth-sessies',
|
||||
'admin.oauthSessions.clientName': 'Client',
|
||||
'admin.oauthSessions.owner': 'Eigenaar',
|
||||
'admin.oauthSessions.scopes': 'Rechten',
|
||||
'admin.oauthSessions.created': 'Aangemaakt',
|
||||
'admin.oauthSessions.empty': 'Geen actieve OAuth-sessies',
|
||||
'admin.oauthSessions.revokeTitle': 'Sessie intrekken',
|
||||
'admin.oauthSessions.revokeMessage': 'Deze OAuth-sessie wordt onmiddellijk ingetrokken. De client verliest MCP-toegang.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sessie ingetrokken',
|
||||
'admin.oauthSessions.revokeError': 'Sessie kon niet worden ingetrokken',
|
||||
'admin.oauthSessions.loadError': 'OAuth-sessies konden niet worden geladen',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -517,9 +517,10 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.weather.locationHint': 'Pogoda jest określana na podstawie pierwszego miejsca z przypisanymi współrzędnymi w danym dniu. Jeśli do dnia nie przypisano żadnego miejsca, jako punkt odniesienia używane jest dowolne miejsce z listy.',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.mcpTokens': 'Tokeny MCP',
|
||||
'admin.mcpTokens.title': 'Tokeny MCP',
|
||||
'admin.mcpTokens.subtitle': 'Zarządzaj tokenami API dla wszystkich użytkowników',
|
||||
'admin.tabs.mcpTokens': 'Dostęp MCP',
|
||||
'admin.mcpTokens.title': 'Dostęp MCP',
|
||||
'admin.mcpTokens.subtitle': 'Zarządzaj sesjami OAuth i tokenami API dla wszystkich użytkowników',
|
||||
'admin.mcpTokens.sectionTitle': 'Tokeny API',
|
||||
'admin.mcpTokens.owner': 'Właściciel',
|
||||
'admin.mcpTokens.tokenName': 'Nazwa tokenu',
|
||||
'admin.mcpTokens.created': 'Utworzono',
|
||||
@@ -531,6 +532,17 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Token został usunięty',
|
||||
'admin.mcpTokens.deleteError': 'Nie udało się usunąć tokenu',
|
||||
'admin.mcpTokens.loadError': 'Nie udało się załadować tokenów',
|
||||
'admin.oauthSessions.sectionTitle': 'Sesje OAuth',
|
||||
'admin.oauthSessions.clientName': 'Klient',
|
||||
'admin.oauthSessions.owner': 'Właściciel',
|
||||
'admin.oauthSessions.scopes': 'Uprawnienia',
|
||||
'admin.oauthSessions.created': 'Utworzono',
|
||||
'admin.oauthSessions.empty': 'Brak aktywnych sesji OAuth',
|
||||
'admin.oauthSessions.revokeTitle': 'Unieważnij sesję',
|
||||
'admin.oauthSessions.revokeMessage': 'Ta sesja OAuth zostanie natychmiast unieważniona. Klient straci dostęp do MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sesja unieważniona',
|
||||
'admin.oauthSessions.revokeError': 'Nie udało się unieważnić sesji',
|
||||
'admin.oauthSessions.loadError': 'Nie udało się załadować sesji OAuth',
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
'admin.audit.subtitle': 'Zdarzenia związane z bezpieczeństwem i administracją (kopie zapasowe, użytkownicy, MFA, ustawienia).',
|
||||
|
||||
@@ -548,9 +548,10 @@ const ru: Record<string, string> = {
|
||||
'admin.weather.locationHint': 'Погода основана на первом месте с координатами в каждом дне. Если ни одно место не назначено на день, в качестве ориентира используется любое место из списка.',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP-токены',
|
||||
'admin.mcpTokens.title': 'MCP-токены',
|
||||
'admin.mcpTokens.subtitle': 'Управление API-токенами всех пользователей',
|
||||
'admin.tabs.mcpTokens': 'MCP-доступ',
|
||||
'admin.mcpTokens.title': 'MCP-доступ',
|
||||
'admin.mcpTokens.subtitle': 'Управление OAuth-сессиями и API-токенами всех пользователей',
|
||||
'admin.mcpTokens.sectionTitle': 'API-токены',
|
||||
'admin.mcpTokens.owner': 'Владелец',
|
||||
'admin.mcpTokens.tokenName': 'Название токена',
|
||||
'admin.mcpTokens.created': 'Создан',
|
||||
@@ -562,6 +563,17 @@ const ru: Record<string, string> = {
|
||||
'admin.mcpTokens.deleteSuccess': 'Токен удалён',
|
||||
'admin.mcpTokens.deleteError': 'Не удалось удалить токен',
|
||||
'admin.mcpTokens.loadError': 'Не удалось загрузить токены',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth-сессии',
|
||||
'admin.oauthSessions.clientName': 'Клиент',
|
||||
'admin.oauthSessions.owner': 'Владелец',
|
||||
'admin.oauthSessions.scopes': 'Права доступа',
|
||||
'admin.oauthSessions.created': 'Создано',
|
||||
'admin.oauthSessions.empty': 'Нет активных OAuth-сессий',
|
||||
'admin.oauthSessions.revokeTitle': 'Отозвать сессию',
|
||||
'admin.oauthSessions.revokeMessage': 'Эта OAuth-сессия будет немедленно отозвана. Клиент потеряет доступ к MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Сессия отозвана',
|
||||
'admin.oauthSessions.revokeError': 'Не удалось отозвать сессию',
|
||||
'admin.oauthSessions.loadError': 'Не удалось загрузить OAuth-сессии',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -548,9 +548,10 @@ const zh: Record<string, string> = {
|
||||
'admin.weather.locationHint': '天气基于每天中第一个有坐标的地点。如果当天没有分配地点,则使用地点列表中的任意地点作为参考。',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP 令牌',
|
||||
'admin.mcpTokens.title': 'MCP 令牌',
|
||||
'admin.mcpTokens.subtitle': '管理所有用户的 API 令牌',
|
||||
'admin.tabs.mcpTokens': 'MCP 访问',
|
||||
'admin.mcpTokens.title': 'MCP 访问',
|
||||
'admin.mcpTokens.subtitle': '管理所有用户的 OAuth 会话和 API 令牌',
|
||||
'admin.mcpTokens.sectionTitle': 'API 令牌',
|
||||
'admin.mcpTokens.owner': '所有者',
|
||||
'admin.mcpTokens.tokenName': '令牌名称',
|
||||
'admin.mcpTokens.created': '创建时间',
|
||||
@@ -562,6 +563,17 @@ const zh: Record<string, string> = {
|
||||
'admin.mcpTokens.deleteSuccess': '令牌已删除',
|
||||
'admin.mcpTokens.deleteError': '删除令牌失败',
|
||||
'admin.mcpTokens.loadError': '加载令牌失败',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth 会话',
|
||||
'admin.oauthSessions.clientName': '客户端',
|
||||
'admin.oauthSessions.owner': '所有者',
|
||||
'admin.oauthSessions.scopes': '权限范围',
|
||||
'admin.oauthSessions.created': '创建时间',
|
||||
'admin.oauthSessions.empty': '暂无活跃的 OAuth 会话',
|
||||
'admin.oauthSessions.revokeTitle': '撤销会话',
|
||||
'admin.oauthSessions.revokeMessage': '此 OAuth 会话将立即被撤销。客户端将失去 MCP 访问权限。',
|
||||
'admin.oauthSessions.revokeSuccess': '会话已撤销',
|
||||
'admin.oauthSessions.revokeError': '撤销会话失败',
|
||||
'admin.oauthSessions.loadError': '加载 OAuth 会话失败',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -532,9 +532,10 @@ const zhTw: Record<string, string> = {
|
||||
'admin.weather.locationHint': '天氣基於每天中第一個有座標的地點。如果當天沒有分配地點,則使用地點列表中的任意地點作為參考。',
|
||||
|
||||
// MCP Tokens
|
||||
'admin.tabs.mcpTokens': 'MCP 令牌',
|
||||
'admin.mcpTokens.title': 'MCP 令牌',
|
||||
'admin.mcpTokens.subtitle': '管理所有使用者的 API 令牌',
|
||||
'admin.tabs.mcpTokens': 'MCP 存取',
|
||||
'admin.mcpTokens.title': 'MCP 存取',
|
||||
'admin.mcpTokens.subtitle': '管理所有使用者的 OAuth 工作階段和 API 令牌',
|
||||
'admin.mcpTokens.sectionTitle': 'API 令牌',
|
||||
'admin.mcpTokens.owner': '所有者',
|
||||
'admin.mcpTokens.tokenName': '令牌名稱',
|
||||
'admin.mcpTokens.created': '建立時間',
|
||||
@@ -546,6 +547,17 @@ const zhTw: Record<string, string> = {
|
||||
'admin.mcpTokens.deleteSuccess': '令牌已刪除',
|
||||
'admin.mcpTokens.deleteError': '刪除令牌失敗',
|
||||
'admin.mcpTokens.loadError': '載入令牌失敗',
|
||||
'admin.oauthSessions.sectionTitle': 'OAuth 工作階段',
|
||||
'admin.oauthSessions.clientName': '客戶端',
|
||||
'admin.oauthSessions.owner': '所有者',
|
||||
'admin.oauthSessions.scopes': '權限範圍',
|
||||
'admin.oauthSessions.created': '建立時間',
|
||||
'admin.oauthSessions.empty': '目前沒有活躍的 OAuth 工作階段',
|
||||
'admin.oauthSessions.revokeTitle': '撤銷工作階段',
|
||||
'admin.oauthSessions.revokeMessage': '此 OAuth 工作階段將立即被撤銷。客戶端將失去 MCP 存取權限。',
|
||||
'admin.oauthSessions.revokeSuccess': '工作階段已撤銷',
|
||||
'admin.oauthSessions.revokeError': '撤銷工作階段失敗',
|
||||
'admin.oauthSessions.loadError': '載入 OAuth 工作階段失敗',
|
||||
|
||||
// GitHub
|
||||
'admin.tabs.github': 'GitHub',
|
||||
|
||||
@@ -307,6 +307,25 @@ router.delete('/mcp-tokens/:id', (req: Request, res: Response) => {
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── OAuth Sessions ─────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/oauth-sessions', (_req: Request, res: Response) => {
|
||||
res.json({ sessions: svc.listOAuthSessions() });
|
||||
});
|
||||
|
||||
router.delete('/oauth-sessions/:id', (req: Request, res: Response) => {
|
||||
const result = svc.revokeOAuthSession(req.params.id);
|
||||
if ('error' in result) return res.status(result.status!).json({ error: result.error });
|
||||
const authReq = req as AuthRequest;
|
||||
writeAudit({
|
||||
userId: authReq.user.id,
|
||||
action: 'admin.oauth_session.revoke',
|
||||
resource: String(req.params.id),
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── JWT Rotation ───────────────────────────────────────────────────────────
|
||||
|
||||
router.post('/rotate-jwt-secret', (req: Request, res: Response) => {
|
||||
@@ -314,12 +333,8 @@ router.post('/rotate-jwt-secret', (req: Request, res: Response) => {
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
const authReq = req as AuthRequest;
|
||||
writeAudit({
|
||||
user_id: authReq.user?.id ?? null,
|
||||
username: authReq.user?.username ?? 'unknown',
|
||||
userId: authReq.user.id,
|
||||
action: 'admin.rotate_jwt_secret',
|
||||
target_type: 'system',
|
||||
target_id: null,
|
||||
details: null,
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { User, Addon } from '../types';
|
||||
import { updateJwtSecret } from '../config';
|
||||
import { maybe_encrypt_api_key, decrypt_api_key } from './apiKeyCrypto';
|
||||
import { getAllPermissions, savePermissions as savePerms, PERMISSION_ACTIONS } from './permissions';
|
||||
import { revokeUserSessions } from '../mcp';
|
||||
import { revokeUserSessions, revokeUserSessionsForClient } from '../mcp';
|
||||
import { validatePassword } from './passwordPolicy';
|
||||
import { getPhotoProviderConfig } from './memories/helpersService';
|
||||
import { send as sendNotification } from './notificationService';
|
||||
@@ -603,6 +603,30 @@ export function deleteMcpToken(id: string) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── OAuth Sessions ─────────────────────────────────────────────────────────
|
||||
|
||||
export function listOAuthSessions() {
|
||||
const rows = db.prepare(`
|
||||
SELECT ot.id, ot.client_id, oc.name AS client_name, ot.user_id, u.username,
|
||||
ot.scopes, ot.access_token_expires_at, ot.refresh_token_expires_at, ot.created_at
|
||||
FROM oauth_tokens ot
|
||||
JOIN oauth_clients oc ON ot.client_id = oc.client_id
|
||||
JOIN users u ON u.id = ot.user_id
|
||||
WHERE ot.revoked_at IS NULL
|
||||
AND ot.refresh_token_expires_at > CURRENT_TIMESTAMP
|
||||
ORDER BY ot.created_at DESC
|
||||
`).all() as (Record<string, unknown> & { scopes: string })[];
|
||||
return rows.map(r => ({ ...r, scopes: JSON.parse(r.scopes) }));
|
||||
}
|
||||
|
||||
export function revokeOAuthSession(id: string) {
|
||||
const row = db.prepare('SELECT id, user_id, client_id FROM oauth_tokens WHERE id = ?').get(id) as { id: number; user_id: number; client_id: string } | undefined;
|
||||
if (!row) return { error: 'Session not found', status: 404 };
|
||||
db.prepare('UPDATE oauth_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
|
||||
revokeUserSessionsForClient(row.user_id, row.client_id);
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── JWT Rotation ───────────────────────────────────────────────────────────
|
||||
|
||||
export function rotateJwtSecret(): { error?: string; status?: number } {
|
||||
|
||||
Reference in New Issue
Block a user