86 lines
3.4 KiB
HTML
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>
|