feat: add AWS Builder ID login and local Kiro cache import

This commit is contained in:
Quorinex
2026-02-04 05:09:42 +08:00
parent 765face800
commit 4f3be1258e
3 changed files with 663 additions and 49 deletions

View File

@@ -169,9 +169,7 @@
<div class="card-header">
<span class="card-title">账号列表</span>
<div class="card-actions">
<button class="btn btn-secondary btn-sm" onclick="showModal('credentials')">导入凭证</button>
<button class="btn btn-secondary btn-sm" onclick="showModal('sso')">SSO</button>
<button class="btn btn-primary btn-sm" onclick="showModal('iam')">IAM</button>
<button class="btn btn-primary btn-sm" onclick="showModal('add')">添加账号</button>
</div>
</div>
<div id="accountsList"></div>
@@ -319,13 +317,13 @@
<div class="account-email">${a.email || a.id.substring(0,12)+'...'}</div>
<div class="account-meta">
${getSubBadge(a.subscriptionType)}
<span class="badge badge-info">${a.authMethod || '-'}</span>
<span class="badge badge-info">${formatAuthMethod(a.provider || a.authMethod)}</span>
${getStatusBadge(a)}
</div>
</div>
<div class="account-actions">
<button class="btn btn-sm btn-icon btn-secondary" onclick="refreshAccount('${a.id}')" title="刷新"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg></button>
<button class="btn btn-sm btn-icon btn-secondary" onclick="showDetail('${a.id}')" title="详情"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg></button>
<button class="btn btn-sm btn-icon btn-secondary" onclick="showDetail('${a.id}')" title="详情"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>
<button class="btn btn-sm ${a.enabled ? 'btn-secondary' : 'btn-primary'}" onclick="toggleAccount('${a.id}',${!a.enabled})">${a.enabled ? '禁用' : '启用'}</button>
<button class="btn btn-sm btn-danger" onclick="deleteAccount('${a.id}')">删除</button>
</div>
@@ -355,6 +353,13 @@
return '<span class="badge badge-free">FREE</span>';
}
function formatAuthMethod(method) {
if (!method) return '-';
if (method === 'idc') return 'Enterprise';
if (method === 'social') return 'Social';
return method;
}
function getStatusBadge(a) {
if (!a.hasToken) return '<span class="badge badge-error">无Token</span>';
if (a.expiresAt && a.expiresAt < Date.now()/1000) return '<span class="badge badge-warning">已过期</span>';
@@ -391,7 +396,7 @@
<div class="detail-grid">
<div class="detail-item"><div class="detail-label">邮箱</div><div class="detail-value">${a.email || '-'}</div></div>
<div class="detail-item"><div class="detail-label">用户ID</div><div class="detail-value">${a.userId || '-'}</div></div>
<div class="detail-item"><div class="detail-label">认证方式</div><div class="detail-value">${a.authMethod || '-'}</div></div>
<div class="detail-item"><div class="detail-label">认证方式</div><div class="detail-value">${formatAuthMethod(a.provider || a.authMethod)}</div></div>
<div class="detail-item"><div class="detail-label">Region</div><div class="detail-value">${a.region || 'us-east-1'}</div></div>
</div>
</div>
@@ -537,33 +542,211 @@
const title = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
if (type === 'credentials') {
title.textContent = '导入凭证';
if (type === 'add') {
title.textContent = '添加账号';
body.innerHTML = `
<div style="display:flex;flex-direction:column;gap:12px">
<div class="card" style="margin:0;cursor:pointer;border:2px solid transparent;transition:border-color 0.2s" onclick="showModal('builderid')" onmouseover="this.style.borderColor='#7c3aed'" onmouseout="this.style.borderColor='transparent'">
<div style="font-weight:600;margin-bottom:6px">AWS Builder ID</div>
<div style="font-size:13px;color:#64748b">通过 AWS Builder ID 授权登录添加个人账号</div>
</div>
<div class="card" style="margin:0;cursor:pointer;border:2px solid transparent;transition:border-color 0.2s" onclick="showModal('iam')" onmouseover="this.style.borderColor='#7c3aed'" onmouseout="this.style.borderColor='transparent'">
<div style="font-weight:600;margin-bottom:6px">IAM Identity Center (企业 SSO) 登录</div>
<div style="font-size:13px;color:#64748b">通过 IAM Identity Center (企业 SSO) 授权添加企业账号</div>
</div>
<div class="card" style="margin:0;cursor:pointer;border:2px solid transparent;transition:border-color 0.2s" onclick="showModal('sso')" onmouseover="this.style.borderColor='#7c3aed'" onmouseout="this.style.borderColor='transparent'">
<div style="font-weight:600;margin-bottom:6px">SSO Token</div>
<div style="font-size:13px;color:#64748b">通过浏览器 x-amz-sso_authn Token 添加账号</div>
</div>
<div class="card" style="margin:0;cursor:pointer;border:2px solid transparent;transition:border-color 0.2s" onclick="showModal('local')" onmouseover="this.style.borderColor='#7c3aed'" onmouseout="this.style.borderColor='transparent'">
<div style="font-weight:600;margin-bottom:6px">Kiro 本地缓存</div>
<div style="font-size:13px;color:#64748b">通过 Kiro IDE 本地缓存文件添加账号</div>
</div>
<div class="card" style="margin:0;cursor:pointer;border:2px solid transparent;transition:border-color 0.2s" onclick="showModal('credentials')" onmouseover="this.style.borderColor='#7c3aed'" onmouseout="this.style.borderColor='transparent'">
<div style="font-weight:600;margin-bottom:6px">凭证 JSON</div>
<div style="font-size:13px;color:#64748b">通过 Kiro Account Manager 导出的凭证添加账号</div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal()">取消</button></div>`;
} else if (type === 'builderid') {
title.textContent = 'AWS Builder ID';
body.innerHTML = `
<p style="font-size:13px;color:#64748b;margin-bottom:16px">通过 AWS Builder ID 授权登录添加个人账号</p>
<div id="builderIdStep1">
<div class="form-group">
<label>Region</label>
<input type="text" id="builderIdRegion" value="us-east-1">
</div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="showModal('add')">返回</button><button class="btn btn-primary" onclick="startBuilderIdLogin()">开始登录</button></div>
</div>
<div id="builderIdStep2" class="hidden">
<div class="message" style="background:#ede9fe;color:#7c3aed;text-align:center">
<p style="font-size:18px;font-weight:600;margin-bottom:8px" id="builderIdUserCode"></p>
<p style="font-size:12px">请在浏览器中输入上方验证码</p>
</div>
<div class="form-group" style="margin-top:16px">
<label>验证链接</label>
<div class="endpoint" style="margin-bottom:0"><span id="builderIdVerifyUrl" style="font-size:12px"></span></div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="btn btn-sm btn-secondary" style="flex:1" onclick="window.open(document.getElementById('builderIdVerifyUrl').textContent,'_blank')">打开</button>
<button class="btn btn-sm btn-secondary" style="flex:1" onclick="navigator.clipboard.writeText(document.getElementById('builderIdVerifyUrl').textContent);alert('已复制')">复制</button>
</div>
</div>
<p id="builderIdStatus" style="color:#64748b;margin:16px 0;font-size:13px;text-align:center">等待授权中...</p>
<div class="modal-footer"><button class="btn btn-secondary" onclick="cancelBuilderIdLogin()">取消</button></div>
</div>`;
} else if (type === 'local') {
title.textContent = 'Kiro 本地缓存';
body.innerHTML = `
<p style="font-size:13px;color:#64748b;margin-bottom:16px">通过 Kiro IDE 本地缓存文件添加账号</p>
<div style="font-size:13px;color:#64748b;margin-bottom:16px;line-height:1.6">
<p style="margin-bottom:8px"><b>文件位置</b></p>
<p style="margin-bottom:4px">Windows: <code style="background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:11px">%USERPROFILE%\\.aws\\sso\\cache\\</code></p>
<p>macOS/Linux: <code style="background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:11px">~/.aws/sso/cache/</code></p>
</div>
<div class="form-group">
<label>登录渠道</label>
<select id="localProvider" onchange="updateLocalFields()">
<option value="BuilderId">AWS Builder ID</option>
<option value="Enterprise">IAM Identity Center (企业 SSO)</option>
<option value="Google">Google</option>
<option value="Github">GitHub</option>
</select>
</div>
<div class="form-group">
<label>kiro-auth-token.json <span style="font-weight:normal;color:#64748b;font-size:12px">*必填</span></label>
<div style="display:flex;gap:8px;align-items:stretch">
<textarea id="localTokenJson" placeholder='粘贴文件内容或上传文件' style="flex:1;min-height:80px;font-size:12px"></textarea>
<label class="btn btn-secondary" style="display:flex;align-items:center;cursor:pointer">
上传<input type="file" accept=".json" style="display:none" onchange="loadLocalFile(this,'localTokenJson')">
</label>
</div>
</div>
<div id="localClientGroup" class="form-group">
<label>{hash}.json <span style="font-weight:normal;color:#64748b;font-size:12px">*IdC 登录必填</span></label>
<div style="display:flex;gap:8px;align-items:stretch">
<textarea id="localClientJson" placeholder='粘贴文件内容或上传文件' style="flex:1;min-height:80px;font-size:12px"></textarea>
<label class="btn btn-secondary" style="display:flex;align-items:center;cursor:pointer">
上传<input type="file" accept=".json" style="display:none" onchange="loadLocalFile(this,'localClientJson')">
</label>
</div>
</div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="showModal('add')">返回</button><button class="btn btn-primary" onclick="importLocalKiro()">添加</button></div>`;
} else if (type === 'credentials') {
title.textContent = '凭证 JSON';
body.innerHTML = `
<p style="font-size:13px;color:#64748b;margin-bottom:16px">通过 Kiro Account Manager 导出的凭证添加账号</p>
<div class="form-group"><label>凭证 JSON</label><textarea id="credJson" placeholder='{"refreshToken":"...","clientId":"...","clientSecret":"..."}'></textarea></div>
<div class="form-group"><label>认证方式</label><select id="credAuth"><option value="social">Social</option><option value="idc">IAM IdC</option></select></div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="importCredentials()">导入</button></div>`;
<div class="form-group">
<label>认证方式 <span style="font-weight:normal;color:#64748b;font-size:12px">*影响 Token 刷新方式</span></label>
<select id="credAuth">
<option value="social">Social (AWS Builder ID / Google / GitHub)</option>
<option value="idc">IAM Identity Center (企业 SSO)</option>
</select>
</div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="showModal('add')">返回</button><button class="btn btn-primary" onclick="importCredentials()">添加</button></div>`;
} else if (type === 'sso') {
title.textContent = 'SSO Token';
body.innerHTML = `
<div class="form-group"><label>Bearer Token</label><textarea id="ssoToken" placeholder="从浏览器获取的 Bearer Token"></textarea></div>
<div style="font-size:13px;color:#64748b;margin-bottom:16px;line-height:1.6">
<p style="margin-bottom:8px"><b>如何获取 Token?</b></p>
<ol style="margin:0;padding-left:20px">
<li>在浏览器中访问并登录 <code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">view.awsapps.com/start</code></li>
<li>按 F12 打开开发者工具 → Application → Cookies</li>
<li>找到并复制 <code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">x-amz-sso_authn</code> 的值</li>
</ol>
</div>
<div class="form-group"><label>x-amz-sso_authn <span style="font-weight:normal;color:#64748b;font-size:12px">*支持批量导入,每行一个 Token</span></label><textarea id="ssoToken" placeholder="粘贴 x-amz-sso_authn 值" style="min-height:120px"></textarea></div>
<div class="form-group"><label>Region</label><input type="text" id="ssoRegion" value="us-east-1"></div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal()">取消</button><button class="btn btn-primary" onclick="importSsoToken()">导入</button></div>`;
<div class="modal-footer"><button class="btn btn-secondary" onclick="showModal('add')">返回</button><button class="btn btn-primary" onclick="importSsoToken()">添加</button></div>`;
} else if (type === 'iam') {
title.textContent = 'IAM SSO 登录';
title.textContent = 'IAM Identity Center (企业 SSO) 登录';
body.innerHTML = `
<p style="font-size:13px;color:#64748b;margin-bottom:16px">通过 IAM Identity Center (企业 SSO) 授权登录添加账号</p>
<div class="form-group"><label>Start URL</label><input type="text" id="iamStartUrl" placeholder="https://xxx.awsapps.com/start"></div>
<div class="form-group"><label>Region</label><input type="text" id="iamRegion" value="us-east-1"></div>
<div id="iamStep2" class="hidden">
<div class="form-group">
<label>登录链接</label>
<div class="endpoint" style="margin-bottom:0"><span id="iamAuthUrl" style="font-size:11px"></span></div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="btn btn-sm btn-secondary" style="flex:1" onclick="window.open(document.getElementById('iamAuthUrl').textContent,'_blank')">打开</button>
<button class="btn btn-sm btn-secondary" style="flex:1" onclick="navigator.clipboard.writeText(document.getElementById('iamAuthUrl').textContent);alert('已复制')">复制</button>
</div>
</div>
<p style="color:#16a34a;margin:12px 0;font-size:14px">请在浏览器中完成登录,然后粘贴回调 URL</p>
<div class="form-group"><label>回调 URL</label><input type="text" id="iamCallback" placeholder="http://127.0.0.1:xxx/?code=..."></div>
</div>
<div class="modal-footer"><button class="btn btn-secondary" onclick="closeModal()">取消</button><button class="btn btn-primary" id="iamBtn" onclick="startIamSso()">开始登录</button></div>`;
<div class="modal-footer"><button class="btn btn-secondary" onclick="showModal('add')">返回</button><button class="btn btn-primary" id="iamBtn" onclick="startIamSso()">开始登录</button></div>`;
}
modal.classList.add('active');
}
function closeModal() { document.getElementById('addModal').classList.remove('active'); iamSession = ''; }
function closeModal() {
document.getElementById('addModal').classList.remove('active');
iamSession = '';
if (builderIdPollTimer) { clearTimeout(builderIdPollTimer); builderIdPollTimer = null; }
builderIdSession = '';
}
function loadLocalFile(input, targetId) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => { document.getElementById(targetId).value = e.target.result; };
reader.readAsText(file);
}
function updateLocalFields() {
const provider = document.getElementById('localProvider').value;
const clientGroup = document.getElementById('localClientGroup');
if (provider === 'Google' || provider === 'Github') {
clientGroup.style.display = 'none';
} else {
clientGroup.style.display = 'block';
}
}
async function importLocalKiro() {
const provider = document.getElementById('localProvider').value;
const tokenJson = document.getElementById('localTokenJson').value.trim();
const clientJson = document.getElementById('localClientJson').value.trim();
const isSocial = provider === 'Google' || provider === 'Github';
if (!tokenJson) { alert('请提供 kiro-auth-token.json 内容'); return; }
let tokenData, clientData;
try {
tokenData = JSON.parse(tokenJson);
} catch { alert('kiro-auth-token.json 格式错误'); return; }
if (!tokenData.refreshToken) { alert('缺少 refreshToken'); return; }
if (!isSocial) {
if (!clientJson) { alert('IdC 登录需要提供 {hash}.json 内容'); return; }
try {
clientData = JSON.parse(clientJson);
} catch { alert('{hash}.json 格式错误'); return; }
if (!clientData.clientId || !clientData.clientSecret) { alert('缺少 clientId 或 clientSecret'); return; }
}
const payload = {
refreshToken: tokenData.refreshToken,
accessToken: tokenData.accessToken || '',
clientId: clientData?.clientId || '',
clientSecret: clientData?.clientSecret || '',
authMethod: isSocial ? 'social' : 'idc',
provider: provider
};
const res = await fetch('/admin/api/auth/credentials', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify(payload)
});
const d = await res.json();
if (d.success) { closeModal(); loadAccounts(); loadStats(); alert('导入成功: ' + (d.account?.email || d.account?.id)); }
else alert('失败: ' + d.error);
}
async function importCredentials() {
try {
@@ -585,8 +768,64 @@
body: JSON.stringify({ bearerToken: document.getElementById('ssoToken').value, region: document.getElementById('ssoRegion').value })
});
const d = await res.json();
if (d.success) { closeModal(); loadAccounts(); loadStats(); alert('导入成功: ' + (d.account?.email || d.account?.id)); }
else alert('失败: ' + d.error);
if (d.success) {
closeModal(); loadAccounts(); loadStats();
const count = d.accounts?.length || 0;
const errCount = d.errors?.length || 0;
let msg = '成功添加 ' + count + ' 个账号';
if (errCount > 0) msg += '' + errCount + ' 个失败';
alert(msg);
} else alert('失败: ' + d.error);
}
let builderIdSession = '';
let builderIdPollTimer = null;
async function startBuilderIdLogin() {
const region = document.getElementById('builderIdRegion').value || 'us-east-1';
const res = await fetch('/admin/api/auth/builderid/start', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ region })
});
const d = await res.json();
if (d.sessionId) {
builderIdSession = d.sessionId;
document.getElementById('builderIdUserCode').textContent = d.userCode;
document.getElementById('builderIdVerifyUrl').textContent = d.verificationUri;
document.getElementById('builderIdStep1').classList.add('hidden');
document.getElementById('builderIdStep2').classList.remove('hidden');
// 开始轮询
pollBuilderIdAuth(d.interval || 5);
} else alert('失败: ' + d.error);
}
function pollBuilderIdAuth(interval) {
builderIdPollTimer = setTimeout(async () => {
const res = await fetch('/admin/api/auth/builderid/poll', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Password': password },
body: JSON.stringify({ sessionId: builderIdSession })
});
const d = await res.json();
if (d.completed) {
closeModal(); loadAccounts(); loadStats();
alert('登录成功: ' + (d.account?.email || d.account?.id));
} else if (d.success && !d.completed) {
document.getElementById('builderIdStatus').textContent = '等待授权中...';
pollBuilderIdAuth(d.interval || interval);
} else {
alert('失败: ' + d.error);
cancelBuilderIdLogin();
}
}, interval * 1000);
}
function cancelBuilderIdLogin() {
if (builderIdPollTimer) {
clearTimeout(builderIdPollTimer);
builderIdPollTimer = null;
}
builderIdSession = '';
showModal('add');
}
let iamSession = '';
@@ -607,7 +846,7 @@
const d = await res.json();
if (d.authorizeUrl) {
iamSession = d.sessionId;
window.open(d.authorizeUrl, '_blank');
document.getElementById('iamAuthUrl').textContent = d.authorizeUrl;
document.getElementById('iamStep2').classList.remove('hidden');
document.getElementById('iamBtn').textContent = '完成登录';
} else alert('失败: ' + d.error);