'use client' import { useState, useEffect, useCallback } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Checkbox } from '@/components/ui/checkbox' import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog' import { useToast } from '@/components/ui/use-toast' import { Loader2, Plus, Copy, Check, Trash2, Key, ChevronDown } from 'lucide-react' import { cn } from '@/lib/utils' import { getBranding } from '@/lib/branding/service' import type { ApiKeyScope } from '@/lib/auth/api-keys' const branding = getBranding() const connectorName = branding.appName.toLowerCase() type ScopeEntry = { scope: ApiKeyScope label: string /** Number of MCP tools gated by this scope. 0 = REST-API-only scope. */ tools: number } type ScopeGroup = { domain: string label: string read: ScopeEntry | null write: ScopeEntry | null } const SCOPE_GROUPS: ScopeGroup[] = [ { domain: 'transactions', label: 'Transaktioner', read: { scope: 'transactions:read', label: 'Läs — lista transaktioner, mallar, kategorier, inbox', tools: 8 }, write: { scope: 'transactions:write', label: 'Skriv — kategorisera, kvittomatchning, koppling mot faktura, dokumentuppladdning', tools: 8 }, }, { domain: 'customers', label: 'Kunder', read: { scope: 'customers:read', label: 'Läs — lista kunder', tools: 1 }, write: { scope: 'customers:write', label: 'Skriv — skapa kunder', tools: 1 }, }, { domain: 'invoices', label: 'Fakturor', read: { scope: 'invoices:read', label: 'Läs — lista fakturor', tools: 1 }, write: { scope: 'invoices:write', label: 'Skriv — skapa, skicka, markera betald/skickad, kreditera, konvertera', tools: 6 }, }, { domain: 'suppliers', label: 'Leverantörer', read: { scope: 'suppliers:read', label: 'Läs — lista leverantörer och leverantörsfakturor', tools: 2 }, write: { scope: 'suppliers:write', label: 'Skriv — godkänn, kreditera, skapa leverantörsfaktura från inbox', tools: 3 }, }, { domain: 'reports', label: 'Rapporter', read: { scope: 'reports:read', label: 'Läs — kontoplan, huvudbok, BR, RR, moms, KPI, reskontra, perioder, bankavstämning, SIE-export', tools: 18 }, write: null, }, { domain: 'bookkeeping', label: 'Bokföring', read: null, write: { scope: 'bookkeeping:write', label: 'Skriv — stänga/låsa perioder, IB, bokslut, SIE-import, verifikat, korrigeringar (alla stagas)', tools: 11 }, }, { domain: 'payroll', label: 'Löner', read: { scope: 'payroll:read', label: 'Läs — lista anställda, lönekörningar, lönejournal', tools: 3 }, write: { scope: 'payroll:write', label: 'Skriv — skapa lönekörning, beräkna, generera AGI', tools: 3 }, }, { domain: 'pending_operations', label: 'Stagade operationer', read: { scope: 'pending_operations:read', label: 'Läs — lista pending_operations som väntar på godkännande', tools: 1 }, write: { scope: 'pending_operations:approve', label: 'Godkänn — committa eller avvisa staged ops via API (ersätter web-UI:s granskning)', tools: 2 }, }, { domain: 'documents', label: 'Dokument (REST API)', read: { scope: 'documents:read', label: 'Läs — lista och hämta dokumentbilagor', tools: 0 }, write: { scope: 'documents:write', label: 'Skriv — ladda upp och koppla dokument till verifikationer', tools: 0 }, }, { domain: 'companies', label: 'Företag (REST API)', read: { scope: 'companies:read', label: 'Läs — företagsprofiler nyckeln har åtkomst till', tools: 0 }, write: null, }, { domain: 'events', label: 'Händelser (REST API)', read: { scope: 'events:read', label: 'Läs — polla event_log som webhook-fallback', tools: 0 }, write: null, }, { domain: 'webhooks', label: 'Webhooks (REST API)', read: null, write: { scope: 'webhooks:manage', label: 'Hantera — skapa, lista, uppdatera, radera prenumerationer', tools: 0 }, }, { domain: 'operations', label: 'Operationer (REST API)', read: { scope: 'operations:read', label: 'Läs — status för långkörande operationer (import, bokslut, omvärdering)', tools: 0 }, write: null, }, { domain: 'compliance', label: 'Compliance (REST API)', read: { scope: 'compliance:read', label: 'Läs — pre-flight: momsstängning, bokslutsberedskap, voucher-gap, IB/UB-kontinuitet', tools: 0 }, write: null, }, ] type Scope = ApiKeyScope const ALL_SCOPES: Scope[] = SCOPE_GROUPS.flatMap((g) => { const out: Scope[] = [] if (g.read) out.push(g.read.scope) if (g.write) out.push(g.write.scope) return out }) interface ApiKey { id: string key_prefix: string name: string scopes: string[] | null rate_limit_rpm: number last_used_at: string | null revoked_at: string | null created_at: string } function CopyBlock({ text }: { text: string }) { const [copied, setCopied] = useState(false) async function handleCopy() { try { await navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { // clipboard unavailable (insecure context) — silently ignore } } return (
        {text}
      
) } function ScopeCard({ entry, checked, onCheckedChange, }: { entry: ScopeEntry checked: boolean onCheckedChange: (checked: boolean) => void }) { const dashIdx = entry.label.indexOf(' — ') const verb = dashIdx > 0 ? entry.label.slice(0, dashIdx) : entry.label const description = dashIdx > 0 ? entry.label.slice(dashIdx + 3) : '' return ( ) } export function ApiKeysPanel() { const { toast } = useToast() const { dialogProps: revokeDialogProps, confirm: confirmRevoke } = useDestructiveConfirm() const [keys, setKeys] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isCreating, setIsCreating] = useState(false) const [showCreateDialog, setShowCreateDialog] = useState(false) const [showKeyDialog, setShowKeyDialog] = useState(false) const [showApiKeyMethods, setShowApiKeyMethods] = useState(false) const [newKeyName, setNewKeyName] = useState('') const [newKeyScopes, setNewKeyScopes] = useState>(new Set(ALL_SCOPES)) const [newKeyValue, setNewKeyValue] = useState('') const [copied, setCopied] = useState(false) const fetchKeys = useCallback(async () => { try { const res = await fetch('/api/settings/api-keys') const json = await res.json() if (json.data) { setKeys(json.data.filter((k: ApiKey) => !k.revoked_at)) } } catch { toast({ title: 'Kunde inte hämta API-nycklar', variant: 'destructive' }) } finally { setIsLoading(false) } }, [toast]) useEffect(() => { fetchKeys() }, [fetchKeys]) async function handleCreate() { setIsCreating(true) try { const res = await fetch('/api/settings/api-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newKeyName || 'MCP-nyckel', scopes: Array.from(newKeyScopes) }), }) const json = await res.json() if (!res.ok) { toast({ title: json.error, variant: 'destructive' }) return } setNewKeyValue(json.data.key) setShowCreateDialog(false) setShowKeyDialog(true) setNewKeyName('') setNewKeyScopes(new Set(ALL_SCOPES)) fetchKeys() } catch { toast({ title: 'Kunde inte skapa nyckel', variant: 'destructive' }) } finally { setIsCreating(false) } } async function handleRevoke(id: string, name: string) { const ok = await confirmRevoke({ title: 'Återkalla API-nyckel', description: `"${name}" återkallas permanent. Alla klienter som använder nyckeln slutar fungera omedelbart.`, confirmLabel: 'Återkalla', }) if (!ok) return try { await fetch(`/api/settings/api-keys/${id}`, { method: 'DELETE' }) setKeys((prev) => prev.filter((k) => k.id !== id)) toast({ title: 'Nyckel återkallad' }) } catch { toast({ title: 'Kunde inte återkalla nyckel', variant: 'destructive' }) } } function handleCopy() { navigator.clipboard.writeText(newKeyValue) setCopied(true) setTimeout(() => setCopied(false), 2000) } function formatDate(iso: string | null) { if (!iso) return '—' return new Date(iso).toLocaleDateString('sv-SE', { year: 'numeric', month: 'short', day: 'numeric', }) } const mcpUrl = typeof window !== 'undefined' ? `${window.location.origin}/api/extensions/ext/mcp-server/mcp` : '/api/extensions/ext/mcp-server/mcp' return (
API-nycklar Hantera nycklar för MCP-klienter (Claude, Cursor) och andra integrationer.
{isLoading ? (
) : keys.length === 0 ? (

Inga API-nycklar ännu.

Skapa en nyckel för att koppla din MCP-klient.

) : (
{keys.map((key) => { const scopeCount = key.scopes?.length ?? 0 return (

{key.name}

{scopeCount === ALL_SCOPES.length ? 'Alla behörigheter' : scopeCount === 0 ? 'Inga behörigheter' : `${scopeCount} behörigheter`}
{key.key_prefix}... Skapad {formatDate(key.created_at)} {key.last_used_at ? `Använd ${formatDate(key.last_used_at)}` : 'Aldrig använd'}
) })}
)}
Anslut MCP-klient

Claude.ai

Rekommenderat

Gå till Settings → Integrations → Add Integration och klistra in MCP-serverns URL. Du loggas in via ditt {connectorName}-konto — ingen API-nyckel behövs.

Claude Code / Cursor

Kör i terminalen — loggar in via webbläsaren:

{showApiKeyMethods && (

Claude Desktop

Lägg till i claude_desktop_config.json (Inställningar → Developer):

Claude Code / Cursor

Kör i terminalen med en API-nyckel:

)}
{/* Create key dialog */} Skapa API-nyckel Ge nyckeln ett namn så du vet vad den används till.
setNewKeyName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleCreate()} />

Välj vad nyckeln ska ha åtkomst till.

{newKeyScopes.size} av {ALL_SCOPES.length} valda
{SCOPE_GROUPS.map((group) => (

{group.label}

{group.read && ( { setNewKeyScopes((prev) => { const next = new Set(prev) if (checked) { next.add(group.read!.scope) } else { next.delete(group.read!.scope) if (group.write) next.delete(group.write.scope) } return next }) }} /> )} {group.write && ( { setNewKeyScopes((prev) => { const next = new Set(prev) if (checked) { next.add(group.write!.scope) if (group.read) next.add(group.read.scope) } else { next.delete(group.write!.scope) } return next }) }} /> )}
))}
{/* Show key once dialog */} { if (!open) { setNewKeyValue('') setCopied(false) } setShowKeyDialog(open) }}> Din nya API-nyckel Kopiera nyckeln nu. Den visas bara en gång.
{newKeyValue}
) }