519423cb0d
- landing/app/mira/wiki: tag=mira-wiki list now shows all wiki paragraphs (depends on KBDB tag filter exposed in matrix/kbdb commit, separate repo) - landing: app/mira hub + feed split + various WIP from prior sessions - registry/components: claude_api / kbdb_create_block / kbdb_get / km_writer / platform_crypto / auth_oauth2 contracts + main.go (accumulated) - .component-builds: pkg-lock updates + index.ts adjustments (WIP) - .agents/specs/arcrun/frontend-redesign: design notes - docs/test_credentials, docs/user_requirements/arcrun-landing-page: WIP docs - cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP) Includes accumulated work from prior sessions plus the wiki UI tag-filter update that surfaces the AI-generated wiki paragraphs at /mira/wiki. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
216 lines
8.2 KiB
TypeScript
216 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
import Link from 'next/link';
|
|
import SiteNav from '../components/SiteNav';
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
|
|
|
|
type User = {
|
|
email: string;
|
|
display_name: string;
|
|
avatar_url?: string;
|
|
api_key: string;
|
|
provider: string;
|
|
created_at: string;
|
|
};
|
|
|
|
export default function DashboardPage() {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [copied, setCopied] = useState(false);
|
|
const [rotating, setRotating] = useState(false);
|
|
const [revoking, setRevoking] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [showKey, setShowKey] = useState(false);
|
|
|
|
const fetchUser = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/me`, { credentials: 'include' });
|
|
if (res.status === 401) {
|
|
window.location.href = '/login?redirect=/dashboard';
|
|
return;
|
|
}
|
|
if (!res.ok) throw new Error('Failed to fetch user');
|
|
const data = await res.json() as User;
|
|
setUser(data);
|
|
} catch {
|
|
setError('無法載入用戶資訊,請重新整理。');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchUser();
|
|
}, [fetchUser]);
|
|
|
|
const copyKey = async () => {
|
|
if (!user) return;
|
|
await navigator.clipboard.writeText(user.api_key);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
const rotateKey = async () => {
|
|
if (!confirm('確定要 Rotate API Key 嗎?舊 Key 的 workflow credentials 不會自動遷移。')) return;
|
|
setRotating(true);
|
|
setError('');
|
|
try {
|
|
const res = await fetch(`${API_BASE}/me/api-key/rotate`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) throw new Error('rotate failed');
|
|
const data = await res.json() as { api_key: string; message: string };
|
|
setUser(prev => prev ? { ...prev, api_key: data.api_key } : null);
|
|
setShowKey(true);
|
|
} catch {
|
|
setError('Rotate 失敗,請稍後重試。');
|
|
} finally {
|
|
setRotating(false);
|
|
}
|
|
};
|
|
|
|
const revokeKey = async () => {
|
|
if (!confirm('確定要 Revoke API Key 嗎?所有使用此 Key 的服務將立即失效。')) return;
|
|
setRevoking(true);
|
|
setError('');
|
|
try {
|
|
const res = await fetch(`${API_BASE}/me/api-key`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) throw new Error('revoke failed');
|
|
window.location.href = '/login?revoked=1';
|
|
} catch {
|
|
setError('Revoke 失敗,請稍後重試。');
|
|
} finally {
|
|
setRevoking(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
|
<div className="text-[#444] text-sm animate-pulse">載入中...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="min-h-screen bg-[#0a0a0a] flex flex-col items-center justify-center gap-4">
|
|
<p className="text-[#666]">{error || '請先登入。'}</p>
|
|
<Link href="/login" className="text-indigo-400 hover:text-indigo-300 text-sm">前往登入</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const maskedKey = showKey ? user.api_key : user.api_key.slice(0, 8) + '••••••••••••••••••••••••';
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0a0a0a] text-[#ededed]">
|
|
<SiteNav currentPath="/dashboard" />
|
|
|
|
<main className="max-w-2xl mx-auto px-6 py-12">
|
|
<h1 className="text-2xl font-bold text-white mb-1">歡迎,{user.display_name}</h1>
|
|
<p className="text-[#555] text-sm mb-10">
|
|
登入方式:{user.provider} · 帳號建立於 {new Date(user.created_at).toLocaleDateString('zh-TW')}
|
|
</p>
|
|
|
|
{error && (
|
|
<div className="bg-red-950/50 border border-red-900/50 text-red-400 text-sm px-4 py-3 rounded-lg mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* API Key Card */}
|
|
<div className="bg-[#111] border border-[#222] rounded-2xl p-6 mb-6">
|
|
<h2 className="text-white font-semibold mb-4">您的 API Key</h2>
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<div className="flex-1 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-3 font-mono text-sm text-[#cdd6f4] overflow-hidden text-ellipsis whitespace-nowrap">
|
|
{maskedKey}
|
|
</div>
|
|
<button
|
|
onClick={() => setShowKey(v => !v)}
|
|
className="px-3 py-3 text-[#555] hover:text-[#aaa] text-xs border border-[#2a2a2a] rounded-lg transition-colors cursor-pointer whitespace-nowrap"
|
|
title={showKey ? '隱藏' : '顯示'}
|
|
>
|
|
{showKey ? '隱藏' : '顯示'}
|
|
</button>
|
|
<button
|
|
onClick={copyKey}
|
|
className="px-4 py-3 bg-[#1e1e2e] hover:bg-[#2a2a3e] text-indigo-400 text-xs border border-indigo-900/30 rounded-lg transition-colors cursor-pointer whitespace-nowrap"
|
|
>
|
|
{copied ? '已複製!' : '複製'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-4 mb-6 text-xs font-mono text-[#666] space-y-1">
|
|
<div className="text-[#444] mb-2"># 使用方式</div>
|
|
<div>Authorization: Bearer {user.api_key.slice(0, 8)}...</div>
|
|
<div># 或</div>
|
|
<div>X-Arcrun-API-Key: {user.api_key.slice(0, 8)}...</div>
|
|
</div>
|
|
|
|
<div className="flex gap-3 flex-wrap">
|
|
<button
|
|
onClick={rotateKey}
|
|
disabled={rotating}
|
|
className="flex-1 border border-[#333] hover:border-[#555] text-[#aaa] hover:text-white px-4 py-2.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
{rotating ? 'Rotating...' : 'Rotate Key'}
|
|
</button>
|
|
<button
|
|
onClick={revokeKey}
|
|
disabled={revoking}
|
|
className="flex-1 border border-red-900/50 hover:border-red-700/50 text-red-500 hover:text-red-400 px-4 py-2.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
{revoking ? 'Revoking...' : 'Revoke Key'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Start */}
|
|
<div className="bg-[#111] border border-[#222] rounded-2xl p-6">
|
|
<h2 className="text-white font-semibold mb-4">快速開始</h2>
|
|
<div className="space-y-3 text-sm">
|
|
<div className="flex items-start gap-3">
|
|
<span className="text-indigo-500 font-mono mt-0.5">1.</span>
|
|
<div>
|
|
<div className="text-[#aaa]">安裝 CLI</div>
|
|
<pre className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg px-3 py-2 mt-1 text-xs text-[#cdd6f4] font-mono">npm install -g arcrun</pre>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<span className="text-indigo-500 font-mono mt-0.5">2.</span>
|
|
<div>
|
|
<div className="text-[#aaa]">初始化(已有 API Key 可直接輸入)</div>
|
|
<pre className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg px-3 py-2 mt-1 text-xs text-[#cdd6f4] font-mono">acr init</pre>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<span className="text-indigo-500 font-mono mt-0.5">3.</span>
|
|
<div>
|
|
<div className="text-[#aaa]">設定服務認證</div>
|
|
<pre className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg px-3 py-2 mt-1 text-xs text-[#cdd6f4] font-mono">acr auth-recipe scaffold notion</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4 mt-6 text-sm">
|
|
<Link href="/integrations" className="text-indigo-400 hover:text-indigo-300 transition-colors">
|
|
查看 20 個支援服務 →
|
|
</Link>
|
|
<Link href="/api-docs" className="text-indigo-400 hover:text-indigo-300 transition-colors">
|
|
API 文件 →
|
|
</Link>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|