Files
iot-dboard/frontend/templates/passkey.html
T
2026-08-06 11:11:11 +08:00

86 lines
3.4 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passkey 验证</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
min-height: 100vh;
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: #0f1117;
color: #fff;
display: flex; align-items: center; justify-content: center;
}
.box { text-align: center; padding: 24px; max-width: 420px; }
.icon { font-size: 44px; margin-bottom: 16px; }
h1 { font-size: 20px; margin-bottom: 10px; font-weight: 600; }
p { font-size: 14px; color: rgba(255,255,255,.55); line-height: 1.7; }
/* 浅色主题覆盖 */
.theme-light { background: #e8edf4; }
.theme-light body { background: #e8edf4; }
.theme-light .box { color: #1c2333; }
.theme-light p { color: rgba(28,35,51,0.65); }
/* 主题切换动画 */
html.theme-anim, html.theme-anim * {
transition: background-color 0.55s ease, color 0.55s ease !important;
}
</style>
<script src="/static/js/theme.js"></script>
</head>
<body>
<div class="box">
<div class="icon">🔑</div>
<h1>Passkey 验证</h1>
<p id="tip">正在准备安全验证…</p>
</div>
<script src="/static/js/passkey_bridge.js"></script>
<script>
// 本页由 iOS 套壳 App 通过 ASWebAuthenticationSession 小窗加载(Safari 环境,
// 原生支持 WebAuthn/Passkey,无需 App 关联域名)。页面从服务端一次性 stash
// 取回 {op, options},走标准 WebAuthn 仪式,完成后跳回 dashboard://passkey。
(function () {
'use strict';
var tip = document.getElementById('tip');
var params = new URLSearchParams(location.search);
var op = params.get('op');
var sid = params.get('sid');
if (!op || !sid) {
tip.textContent = '缺少 Passkey 参数,请返回重试。';
return;
}
fetch('/api/passkey/stash/' + encodeURIComponent(sid))
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.options) throw new Error(data.error || 'Passkey 参数已失效');
var options = data.options;
tip.textContent = '请在系统 Passkey 面板中完成验证…';
// 中间件:本页运行于小窗(Safari 环境),PasskeyBridge 判定为浏览器模式,
// 直接调用 navigator.credentials;返回纯 base64url JSON credential。
return (op === 'creation' ? PasskeyBridge.creation(options) : PasskeyBridge.assertion(options))
.then(function (credential) {
var user_name = (options.user && options.user.name) ? options.user.name : '';
// credential 较大,POST 回服务端;回调 URL 只带短 sid(避免 URL 截断)
return fetch('/api/passkey/stash/' + encodeURIComponent(sid) + '/result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credential, user_name: user_name })
}).then(function (r) { return r.json(); })
.then(function (res) {
if (!res.ok) throw new Error(res.error || '结果保存失败');
location.href = 'dashboard://passkey?sid=' + encodeURIComponent(sid);
});
});
})
.catch(function (err) {
location.href = 'dashboard://passkey?error=' + encodeURIComponent((err && err.message) || 'Passkey 验证失败');
});
})();
</script>
</body>
</html>