05/18 Enhanced codes and functionalities 11
This commit is contained in:
+14
-1
@@ -78,11 +78,24 @@ def register_begin():
|
||||
"""
|
||||
Generate PublicKeyCredentialCreationOptions for passkey registration.
|
||||
Requires an active JWT session (user must be logged in).
|
||||
|
||||
Optional JSON body:
|
||||
attachment: "platform" (default) | "cross-platform"
|
||||
"platform" → device biometrics (Touch ID, Face ID, Windows Hello)
|
||||
"cross-platform" → roaming authenticators (YubiKey, phone-as-key via QR)
|
||||
"""
|
||||
user = db.session.get(User, g.current_user_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
attachment_str = (data.get('attachment') or 'platform').strip().lower()
|
||||
|
||||
if attachment_str == 'cross-platform':
|
||||
authenticator_attachment = AuthenticatorAttachment.CROSS_PLATFORM
|
||||
else:
|
||||
authenticator_attachment = AuthenticatorAttachment.PLATFORM
|
||||
|
||||
# Collect existing credential IDs to exclude (prevent re-registering same key).
|
||||
existing = WebAuthnCredential.query.filter_by(user_id=user.id).all()
|
||||
exclude_credentials = [
|
||||
@@ -102,7 +115,7 @@ def register_begin():
|
||||
authenticator_selection=AuthenticatorSelectionCriteria(
|
||||
resident_key=ResidentKeyRequirement.PREFERRED,
|
||||
user_verification=UserVerificationRequirement.PREFERRED,
|
||||
authenticator_attachment=AuthenticatorAttachment.PLATFORM,
|
||||
authenticator_attachment=authenticator_attachment,
|
||||
),
|
||||
exclude_credentials=exclude_credentials,
|
||||
)
|
||||
|
||||
@@ -2550,3 +2550,21 @@ html.sidebar-open {
|
||||
.health-banner-dismiss:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Passkey attachment type selector */
|
||||
.passkey-attachment-select {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.passkey-attachment-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);
|
||||
}
|
||||
|
||||
@@ -523,9 +523,10 @@ const PasskeyAuth = (() => {
|
||||
/**
|
||||
* Register a new passkey for the currently logged-in user.
|
||||
* Requires an active access_token in sessionStorage (set by vault.js on login).
|
||||
* @param {string} name User-friendly name for the passkey (e.g. "iPhone 15").
|
||||
* @param {string} name User-friendly name (e.g. "iPhone 15").
|
||||
* @param {string} attachment "platform" (default) | "cross-platform"
|
||||
*/
|
||||
async function registerPasskey(name) {
|
||||
async function registerPasskey(name, attachment = "platform") {
|
||||
if (!window.PublicKeyCredential) {
|
||||
return { error: 'Passkeys are not supported in this browser.' };
|
||||
}
|
||||
@@ -540,6 +541,7 @@ const PasskeyAuth = (() => {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ attachment }),
|
||||
});
|
||||
if (!beginRes.ok) {
|
||||
const d = await beginRes.json().catch(() => ({}));
|
||||
|
||||
+10
-2
@@ -2442,10 +2442,16 @@ const Vault = (() => {
|
||||
const lastUsed = c.last_used_at
|
||||
? `Last used ${new Date(c.last_used_at).toLocaleDateString()}`
|
||||
: "Never used";
|
||||
const transports = c.transports || [];
|
||||
const typeLabel = transports.includes("internal")
|
||||
? "📱 Device"
|
||||
: transports.some((t) => ["usb", "nfc", "ble", "smart-card"].includes(t))
|
||||
? "🔑 Security key"
|
||||
: "🔑 Passkey";
|
||||
return `<div class="passkey-item" data-cred-id="${c.id}">
|
||||
<div class="passkey-info">
|
||||
<span class="passkey-name">${escHtml(c.name)}</span>
|
||||
<span class="passkey-meta">${lastUsed} · Added ${escHtml(created)}</span>
|
||||
<span class="passkey-meta">${typeLabel} · ${lastUsed} · Added ${escHtml(created)}</span>
|
||||
</div>
|
||||
<div class="passkey-actions">
|
||||
<button class="btn-text btn-sm btn-rename-passkey" data-id="${c.id}" data-name="${escHtml(c.name)}">Rename</button>
|
||||
@@ -2498,13 +2504,15 @@ const Vault = (() => {
|
||||
registerBtn.dataset.bound = "1";
|
||||
registerBtn.addEventListener("click", async () => {
|
||||
const nameInput = document.getElementById("passkey-name-input");
|
||||
const attachSel = document.getElementById("passkey-attachment-select");
|
||||
const name = (nameInput?.value || "").trim() || "Passkey";
|
||||
const attachment = attachSel?.value || "platform";
|
||||
const errEl = document.getElementById("passkeys-error");
|
||||
errEl?.classList.add("hidden");
|
||||
registerBtn.disabled = true;
|
||||
registerBtn.textContent = "Waiting…";
|
||||
|
||||
const result = await PasskeyAuth.registerPasskey(name);
|
||||
const result = await PasskeyAuth.registerPasskey(name, attachment);
|
||||
|
||||
registerBtn.disabled = false;
|
||||
registerBtn.textContent = "+ Add passkey";
|
||||
|
||||
@@ -977,6 +977,10 @@
|
||||
maxlength="128"
|
||||
class="passkey-name-input"
|
||||
/>
|
||||
<select id="passkey-attachment-select" class="passkey-attachment-select">
|
||||
<option value="platform">Device (biometric)</option>
|
||||
<option value="cross-platform">Security key / cross-device</option>
|
||||
</select>
|
||||
<button class="btn-secondary" id="btn-register-passkey">
|
||||
+ Add passkey
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user