2026-05-28 16:39:51 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<title>上传凭证 FileAuth</title>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<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; }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="wrap">
|
|
|
|
|
|
<header>
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<h1>上传凭证(FileAuth.java)</h1>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<nav class="nav" id="main-nav"></nav>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="formula">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
与 <code>docs/FileAuth.java</code> 一致:
|
|
|
|
|
|
<code>sign = HMAC-SHA1(secret, policyJSON)</code> →
|
|
|
|
|
|
<code>uploadToken = AccessKey:encodedSign:policyB64</code>(policyB64 = urlsafe_base64(policyJSON))
|
2026-05-28 16:39:51 +08:00
|
|
|
|
</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>
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<button class="primary" id="btn-gen">生成</button>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<div class="panel" id="panel-upload">
|
|
|
|
|
|
<h3>生产 upload profile(scope=仅 bucket)</h3>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<div class="step">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<h4>1. putPolicy</h4>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<pre id="policy-json">—</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<h4>2. sign = HMAC-SHA1(secret, <strong>policyJSON</strong> UTF-8) → encodedSign</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>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<h4>3. policyB64 = urlsafe_base64(putPolicy)</h4>
|
|
|
|
|
|
<pre id="encoded-policy">—</pre>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<h4>4. uploadToken = AccessKey + ':' + encodedSign + ':' + policyB64</h4>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<pre id="upload-token">—</pre>
|
|
|
|
|
|
</div>
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<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></p>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<h3>格式校验</h3>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
<p id="format-summary">—</p>
|
|
|
|
|
|
<ul id="format-checks"></ul>
|
2026-05-28 17:03:08 +08:00
|
|
|
|
<p>与 Runner 当前 token:<span id="runner-match">—</span>(<code id="runner-preview">—</code>)</p>
|
2026-05-28 16:39:51 +08:00
|
|
|
|
</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 || '—';
|
|
|
|
|
|
|
|
|
|
|
|
const sum = document.getElementById('format-summary');
|
|
|
|
|
|
sum.innerHTML = data.formatOk
|
2026-05-28 17:03:08 +08:00
|
|
|
|
? '<span class="badge ok">格式符合 AccessKey:encodedSign:policyB64</span>'
|
2026-05-28 16:39:51 +08:00
|
|
|
|
: '<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 || '—';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('btn-gen').onclick = () => load(false);
|
|
|
|
|
|
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>
|