404 lines
8.4 KiB
HTML
404 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
|
||
<meta charset="utf-8">
|
||
|
||
<title>上传凭证 §2.3.3</title>
|
||
|
||
<link rel="stylesheet" href="/static/static.css">
|
||
|
||
<style>
|
||
|
||
.step { margin-bottom: 1rem; }
|
||
|
||
.step h4 { margin: 0 0 0.35rem; color: var(--accent); font-size: 0.95rem; }
|
||
|
||
.step pre { margin: 0; max-height: 12rem; overflow: auto; }
|
||
|
||
.formula { color: var(--muted); font-size: 0.9rem; margin-bottom: 1rem; }
|
||
|
||
.check-ok { color: var(--ok); }
|
||
|
||
.check-fail { color: var(--err); }
|
||
|
||
label.chk { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; }
|
||
|
||
.diff { border-left: 3px solid var(--warn); padding-left: 0.75rem; margin-top: 0.5rem; }
|
||
|
||
</style>
|
||
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<div class="wrap">
|
||
|
||
<header>
|
||
|
||
<h1>上传凭证(Ver2.0 §2.3.3)</h1>
|
||
|
||
<nav class="nav" id="main-nav"></nav>
|
||
|
||
</header>
|
||
|
||
|
||
|
||
<p class="formula">
|
||
|
||
五步:<code>putPolicy</code> → <code>encodedPutPolicy</code>(URL-safe Base64)→
|
||
|
||
<code>HMAC-SHA1(secret, encodedPutPolicy)</code> → <code>encodedSign</code> →
|
||
|
||
<code>uploadToken = AccessKey:encodedSign:encodedPutPolicy</code>
|
||
|
||
</p>
|
||
|
||
|
||
|
||
<div class="toolbar">
|
||
|
||
<label>deadline(Unix 秒)<input type="text" id="deadline" placeholder="默认 now+3600" style="width:11rem"></label>
|
||
|
||
<label>bucket <input type="text" id="bucket" placeholder="Runner fileBucket" style="width:10rem"></label>
|
||
|
||
<button class="primary" id="btn-gen">生成(upload profile)</button>
|
||
|
||
<button id="btn-doc">文档黄金样例</button>
|
||
|
||
<button id="btn-refresh">刷新 Runner 并对比</button>
|
||
|
||
<label class="chk"><input type="checkbox" id="show-full"> 显示完整 token</label>
|
||
|
||
<button type="button" id="btn-copy">复制完整 token</button>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div id="err-box"></div>
|
||
|
||
|
||
|
||
<div class="panel" id="panel-ver233">
|
||
|
||
<h3>A. §2.3.3 生产算法(transit / xk-api)</h3>
|
||
|
||
<div class="step">
|
||
|
||
<h4>1. putPolicy(scope=仅 bucket,无 returnBody)</h4>
|
||
|
||
<pre id="policy-json">—</pre>
|
||
|
||
</div>
|
||
|
||
<div class="step">
|
||
|
||
<h4>2. encodedPutPolicy = urlsafe_base64(putPolicy)</h4>
|
||
|
||
<pre id="encoded-policy">—</pre>
|
||
|
||
</div>
|
||
|
||
<div class="step">
|
||
|
||
<h4>3. sign = HMAC-SHA1(secret, <strong>encodedPutPolicy</strong> UTF-8)</h4>
|
||
|
||
<p class="formula" style="margin:0 0 0.35rem">signInput(签名字节串):<code id="sign-input">—</code> · hex:<code id="sign-hex">—</code></p>
|
||
|
||
<pre id="encoded-sign">—</pre>
|
||
|
||
</div>
|
||
|
||
<div class="step">
|
||
|
||
<h4>4–5. uploadToken = AccessKey + ':' + encodedSign + ':' + encodedPutPolicy</h4>
|
||
|
||
<pre id="upload-token">—</pre>
|
||
|
||
</div>
|
||
|
||
<p>算法:<code id="algorithm">—</code> · B64:<code id="b64-mode">—</code> · AccessKey:<code id="access-key">—</code> · scope:<code id="scope-val">—</code> · deadline:<code id="deadline-val">—</code>
|
||
|
||
· <span id="doc-golden">—</span></p>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="panel">
|
||
|
||
<h3>B. 格式校验</h3>
|
||
|
||
<p id="format-summary">—</p>
|
||
|
||
<ul id="format-checks"></ul>
|
||
|
||
<p>与 Runner 当前 token(默认 prescription.pdf):<span id="runner-match">—</span>(<code id="runner-preview">—</code>)</p>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="panel">
|
||
|
||
<h3>C. 监管文档教学示例(doc profile,只读)</h3>
|
||
|
||
<p id="doc-note" class="formula">—</p>
|
||
|
||
<div class="step">
|
||
|
||
<h4>示例 token 三段</h4>
|
||
|
||
<pre id="doc-token-parts">—</pre>
|
||
|
||
</div>
|
||
|
||
<div class="step">
|
||
|
||
<h4>encodedPutPolicy 解码</h4>
|
||
|
||
<pre id="doc-policy">—</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script src="/static/app.js"></script>
|
||
|
||
<script>
|
||
|
||
if (typeof renderMainNav === 'function') renderMainNav('fileauth');
|
||
|
||
|
||
|
||
let lastData = null;
|
||
|
||
|
||
|
||
function tokenDisplay(data) {
|
||
|
||
if (!data) return '—';
|
||
|
||
const show = document.getElementById('show-full').checked;
|
||
|
||
return show ? data.uploadToken : (data.uploadTokenMasked || data.uploadToken);
|
||
|
||
}
|
||
|
||
|
||
|
||
function renderChecks(checks) {
|
||
|
||
const ul = document.getElementById('format-checks');
|
||
|
||
ul.innerHTML = (checks || []).map(c => {
|
||
|
||
const cls = c.ok ? 'check-ok' : 'check-fail';
|
||
|
||
const mark = c.ok ? '✓' : '✗';
|
||
|
||
return `<li class="${cls}">${mark} <strong>${esc(c.id)}</strong> — ${esc(c.detail)}</li>`;
|
||
|
||
}).join('');
|
||
|
||
}
|
||
|
||
|
||
|
||
function render(data) {
|
||
|
||
lastData = data;
|
||
|
||
document.getElementById('err-box').innerHTML = '';
|
||
|
||
document.getElementById('policy-json').textContent = data.policyJson || '—';
|
||
|
||
document.getElementById('encoded-policy').textContent = data.encodedPutPolicy || '—';
|
||
|
||
document.getElementById('sign-input').textContent = data.signInput || '—';
|
||
|
||
document.getElementById('sign-hex').textContent = data.signHex || '—';
|
||
|
||
document.getElementById('encoded-sign').textContent = data.encodedSign || '—';
|
||
|
||
document.getElementById('upload-token').textContent = tokenDisplay(data);
|
||
|
||
document.getElementById('access-key').textContent = data.accessKey || '—';
|
||
|
||
document.getElementById('scope-val').textContent = data.scope || data.bucket || '—';
|
||
|
||
document.getElementById('b64-mode').textContent = data.b64Mode || '—';
|
||
|
||
document.getElementById('deadline-val').textContent = data.deadlineUnix != null ? String(data.deadlineUnix) : '—';
|
||
|
||
document.getElementById('algorithm').textContent = data.algorithm || '—';
|
||
|
||
document.getElementById('doc-golden').innerHTML = data.docGoldenMatch
|
||
|
||
? '<span class="badge ok">与文档黄金样例一致</span>'
|
||
|
||
: '<span class="badge info">非文档固定向量(点「文档黄金样例」可对照)</span>';
|
||
|
||
|
||
|
||
const sum = document.getElementById('format-summary');
|
||
|
||
sum.innerHTML = data.formatOk
|
||
|
||
? '<span class="badge ok">格式符合 AccessKey:encodedSign:encodedPutPolicy</span>'
|
||
|
||
: '<span class="badge err">格式校验未全部通过</span>';
|
||
|
||
renderChecks(data.formatChecks);
|
||
|
||
|
||
|
||
const matchEl = document.getElementById('runner-match');
|
||
|
||
if (data.runnerTokenMatch) {
|
||
|
||
matchEl.innerHTML = '<span class="badge ok">一致</span>';
|
||
|
||
} else {
|
||
|
||
matchEl.innerHTML = '<span class="badge warn">不一致</span>(自定义 deadline 或需重新拉取配置)';
|
||
|
||
}
|
||
|
||
document.getElementById('runner-preview').textContent = data.runnerTokenPreview || '—';
|
||
|
||
|
||
|
||
const doc = data.docExample || {};
|
||
|
||
document.getElementById('doc-note').textContent = doc.note || '—';
|
||
|
||
document.getElementById('doc-token-parts').textContent =
|
||
|
||
'AccessKey: ' + (doc.accessKeyPart || '') + '\n' +
|
||
|
||
'encodedSign: ' + (doc.encodedSignPart || '') + '\n' +
|
||
|
||
'encodedPutPolicy: ' + (doc.encodedPolicyPart || '');
|
||
|
||
document.getElementById('doc-policy').textContent = doc.policyJsonDecoded || '—';
|
||
|
||
}
|
||
|
||
|
||
|
||
async function load(refresh) {
|
||
|
||
const errBox = document.getElementById('err-box');
|
||
|
||
errBox.innerHTML = '';
|
||
|
||
const params = new URLSearchParams();
|
||
|
||
const dl = document.getElementById('deadline').value.trim();
|
||
|
||
const bk = document.getElementById('bucket').value.trim();
|
||
|
||
if (dl) params.set('deadline', dl);
|
||
|
||
if (bk) params.set('bucket', bk);
|
||
|
||
if (refresh) params.set('refresh', '1');
|
||
|
||
try {
|
||
|
||
const data = await api('/api/fileauth/generate?' + params.toString());
|
||
|
||
render(data);
|
||
|
||
showToast('已生成', 'ok');
|
||
|
||
} catch (e) {
|
||
|
||
showInlineError(errBox, e.message || String(e));
|
||
|
||
showToast(firstLine(e.message) || '生成失败', 'err');
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
async function loadDocGolden() {
|
||
|
||
document.getElementById('deadline').value = '1451491200';
|
||
|
||
document.getElementById('bucket').value = 'my-bucket';
|
||
|
||
try {
|
||
|
||
const data = await api('/api/fileauth/generate?doc=1');
|
||
|
||
render(data);
|
||
|
||
showToast('文档黄金样例', 'ok');
|
||
|
||
} catch (e) {
|
||
|
||
showInlineError(document.getElementById('err-box'), e.message || String(e));
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
document.getElementById('btn-gen').onclick = () => load(false);
|
||
|
||
document.getElementById('btn-doc').onclick = () => loadDocGolden();
|
||
|
||
document.getElementById('btn-refresh').onclick = () => load(true);
|
||
|
||
document.getElementById('show-full').onchange = () => {
|
||
|
||
if (lastData) document.getElementById('upload-token').textContent = tokenDisplay(lastData);
|
||
|
||
};
|
||
|
||
document.getElementById('btn-copy').onclick = async () => {
|
||
|
||
if (!lastData || !lastData.uploadToken) {
|
||
|
||
showToast('请先生成 token', 'warn');
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
try {
|
||
|
||
await navigator.clipboard.writeText(lastData.uploadToken);
|
||
|
||
showToast('已复制完整 uploadToken', 'ok');
|
||
|
||
} catch {
|
||
|
||
showToast('复制失败', 'err');
|
||
|
||
}
|
||
|
||
};
|
||
|
||
|
||
|
||
load(false);
|
||
|
||
</script>
|
||
|
||
</body>
|
||
|
||
</html>
|
||
|
||
|