feat(console): API Key 介面錯位收斂為簡單 email+password 登入 (Arcrun#3 發現②)
背景:self-hosted 單租戶下 console 原本那格「API Key」其實只是 namespace 明碼字串,不是註冊制 key(leo 原話:理論上根本沒有 API Key 這件事)。leo 拍板: 換成簡單 email+password 登入頁(自己設一組帳密,不用 OAuth),登入成功後端發 session token 存 localStorage;後端 API 呼叫仍用固定租戶字串打 KBDB(登入系統 只擋外人看頁面,不做多租戶)。 租戶字串收斂:發現①已核實 owner_id='leo' 是 D1 中 458,357 筆資料實際使用的 租戶字串(ak_... 只有 2 筆孤兒資料)。CONSOLE_TENANT 預設 "leo",不製造第三個租戶。 新增: - cypher-executor/src/routes/console-auth.ts:/console/auth-status、 /console/setup(首次自助設定帳密,寫入 SESSIONS_KV console:credentials)、 /console/setup/reset(換帳密,需舊密碼)、/console/login、/console/session (驗 session + 回傳固定租戶字串)、/console/logout。密碼用 salt + 3 輪 SHA-256 雜湊,不存明碼。 - cypher-executor/src/types.ts:Bindings 加 CONSOLE_TENANT。 - cypher-executor/wrangler.toml:[vars] 加 CONSOLE_TENANT = "leo"。 - cypher-executor/src/routes/console.ts:① 卡片從「貼 API Key」改成登入/首次 設定表單;查詢函式改用登入後端回的固定租戶字串,使用者不再需要知道任何 namespace 字串。 驗證見 issue #3 留言。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,12 @@
|
||||
* 不新增 binding;CORS 該 worker已開;不需 auth。
|
||||
* recipes 打同源 GET /public-recipes(recipes.ts,公庫,不需 auth)
|
||||
*
|
||||
* 認證:頁面請用戶貼一次 X-Arcrun-API-Key(self-hosted=namespace 明碼),存 localStorage。
|
||||
* knowledge/workflows 兩區需要;components/recipes 是公開資料,不需要。
|
||||
* 認證(v1,Arcrun#3 發現②改版):不再讓使用者貼 API Key(self-hosted 單租戶下那格本來就只是
|
||||
* namespace 明碼字串,不是註冊制 key——leo 原話:理論上根本沒有 API Key 這件事)。改用簡單
|
||||
* email+password 登入頁(routes/console-auth.ts,自己設一組帳密,無第三方 OAuth)。登入成功後端
|
||||
* 發 session token 存 localStorage;**實際打 /kbdb/*、/workflows/search 仍用固定租戶字串**
|
||||
* (後端回應帶的 tenant,來自 CONSOLE_TENANT,登入系統只擋外人看頁面,不做多租戶)。
|
||||
* knowledge/workflows 兩區需要登入;components/recipes 是公開資料,不需要。
|
||||
*
|
||||
* config 區(v0 唯讀):vectorize 狀態不是新端點——直接讀「知識庫」查詢回應本身的 mode/
|
||||
* capability_hint(kbdb entries.ts 既有機制:要求 semantic、缺 Vectorize 就誠實降級並帶 hint)。
|
||||
@@ -68,12 +72,35 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
</header>
|
||||
<main>
|
||||
<div class="card" id="auth-card">
|
||||
<h2>① API Key</h2>
|
||||
<div class="row">
|
||||
<input type="password" id="api-key" placeholder="貼你的 X-Arcrun-API-Key(self-hosted=namespace 明碼)">
|
||||
<button id="save-key">存起來</button>
|
||||
<h2>① 登入</h2>
|
||||
<div id="auth-loading" class="hint">檢查登入狀態中...</div>
|
||||
|
||||
<div id="auth-setup-form" style="display:none">
|
||||
<div class="hint">首次使用,設定你自己的登入帳密(只存在這個 worker 的 KV,非第三方 OAuth)。</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<input type="text" id="setup-email" placeholder="email">
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<input type="password" id="setup-password" placeholder="密碼(至少 8 碼)">
|
||||
<button id="setup-submit">設定並登入</button>
|
||||
</div>
|
||||
<div id="setup-status" class="hint"></div>
|
||||
</div>
|
||||
|
||||
<div id="auth-login-form" style="display:none">
|
||||
<div class="row">
|
||||
<input type="text" id="login-email" placeholder="email">
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<input type="password" id="login-password" placeholder="密碼">
|
||||
<button id="login-submit">登入</button>
|
||||
</div>
|
||||
<div id="login-status" class="hint"></div>
|
||||
</div>
|
||||
|
||||
<div id="auth-authed" style="display:none">
|
||||
<div class="hint">已登入 <span id="authed-tenant"></span> <button class="secondary" id="logout-btn" style="margin-left:8px">登出</button></div>
|
||||
</div>
|
||||
<div class="hint">存在瀏覽器 localStorage,不送去別的地方。知識庫/workflows 查詢需要它;components/recipes 是公庫資料不用。</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -115,15 +142,97 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
const REGISTRY_BASE = ${JSON.stringify(registryBase)};
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function getKey() { return localStorage.getItem('arcrun_console_api_key') || ''; }
|
||||
function setKey(v) { localStorage.setItem('arcrun_console_api_key', v); }
|
||||
// ── 登入狀態(Arcrun#3 發現②:session token 只擋頁面,實際查詢用後端回的固定租戶字串)──
|
||||
let currentTenant = ''; // 登入成功後才有值;查詢函式用它當 X-Arcrun-API-Key
|
||||
function getSessionToken() { return localStorage.getItem('arcrun_console_session') || ''; }
|
||||
function setSessionToken(v) { localStorage.setItem('arcrun_console_session', v); }
|
||||
function clearSessionToken() { localStorage.removeItem('arcrun_console_session'); }
|
||||
function getKey() { return currentTenant; } // 給 searchKb/searchWorkflows 沿用既有介面
|
||||
|
||||
$('api-key').value = getKey();
|
||||
$('save-key').addEventListener('click', () => {
|
||||
setKey($('api-key').value.trim());
|
||||
$('api-key').placeholder = '已存';
|
||||
function showAuthPanel(which) {
|
||||
['auth-loading', 'auth-setup-form', 'auth-login-form', 'auth-authed'].forEach((id) => {
|
||||
$(id).style.display = id === which ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function checkAuthStatus() {
|
||||
const token = getSessionToken();
|
||||
if (token) {
|
||||
try {
|
||||
const res = await fetch('/console/session', { headers: { Authorization: 'Bearer ' + token } });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
currentTenant = data.tenant || '';
|
||||
$('authed-tenant').textContent = '(租戶:' + currentTenant + ')';
|
||||
showAuthPanel('auth-authed');
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fall through to login */ }
|
||||
clearSessionToken();
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/console/auth-status');
|
||||
const data = await res.json();
|
||||
showAuthPanel(data.configured ? 'auth-login-form' : 'auth-setup-form');
|
||||
} catch (e) {
|
||||
showAuthPanel('auth-login-form');
|
||||
}
|
||||
}
|
||||
|
||||
$('setup-submit').addEventListener('click', async () => {
|
||||
const email = $('setup-email').value.trim();
|
||||
const password = $('setup-password').value;
|
||||
$('setup-status').textContent = '設定中...';
|
||||
try {
|
||||
const res = await fetch('/console/setup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { $('setup-status').textContent = data.error || '設定失敗'; return; }
|
||||
setSessionToken(data.session_token);
|
||||
currentTenant = data.tenant || '';
|
||||
$('authed-tenant').textContent = '(租戶:' + currentTenant + ')';
|
||||
showAuthPanel('auth-authed');
|
||||
} catch (e) {
|
||||
$('setup-status').textContent = '請求失敗:' + e.message;
|
||||
}
|
||||
});
|
||||
|
||||
$('login-submit').addEventListener('click', async () => {
|
||||
const email = $('login-email').value.trim();
|
||||
const password = $('login-password').value;
|
||||
$('login-status').textContent = '登入中...';
|
||||
try {
|
||||
const res = await fetch('/console/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { $('login-status').textContent = data.error || '登入失敗'; return; }
|
||||
setSessionToken(data.session_token);
|
||||
currentTenant = data.tenant || '';
|
||||
$('authed-tenant').textContent = '(租戶:' + currentTenant + ')';
|
||||
showAuthPanel('auth-authed');
|
||||
} catch (e) {
|
||||
$('login-status').textContent = '請求失敗:' + e.message;
|
||||
}
|
||||
});
|
||||
|
||||
$('logout-btn').addEventListener('click', async () => {
|
||||
const token = getSessionToken();
|
||||
if (token) {
|
||||
try { await fetch('/console/logout', { method: 'POST', headers: { Authorization: 'Bearer ' + token } }); } catch (e) {}
|
||||
}
|
||||
clearSessionToken();
|
||||
currentTenant = '';
|
||||
checkAuthStatus();
|
||||
});
|
||||
|
||||
checkAuthStatus();
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
@@ -160,7 +269,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
const ul = $('kb-results');
|
||||
if (!q) { statusEl.textContent = '請輸入查詢字'; return; }
|
||||
const key = getKey();
|
||||
if (!key) { statusEl.textContent = '請先貼 API Key(上方 ①)'; return; }
|
||||
if (!key) { statusEl.textContent = '請先登入(上方 ①)'; return; }
|
||||
statusEl.textContent = '查詢中...';
|
||||
try {
|
||||
const res = await fetch('/kbdb/search?q=' + encodeURIComponent(q) + '&mode=semantic', {
|
||||
@@ -191,7 +300,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
const ul = $('wf-results');
|
||||
if (!q) { statusEl.textContent = '請輸入查詢字'; return; }
|
||||
const key = getKey();
|
||||
if (!key) { statusEl.textContent = '請先貼 API Key(上方 ①)'; return; }
|
||||
if (!key) { statusEl.textContent = '請先登入(上方 ①)'; return; }
|
||||
statusEl.textContent = '查詢中...';
|
||||
try {
|
||||
const res = await fetch('/workflows/search?q=' + encodeURIComponent(q), {
|
||||
|
||||
Reference in New Issue
Block a user