05/18 Enhanced codes and functionalities 11

This commit is contained in:
2026-05-18 22:00:42 -04:00
parent 78a96e092f
commit 6177b72ef8
5 changed files with 54 additions and 9 deletions
+14 -1
View File
@@ -78,11 +78,24 @@ def register_begin():
""" """
Generate PublicKeyCredentialCreationOptions for passkey registration. Generate PublicKeyCredentialCreationOptions for passkey registration.
Requires an active JWT session (user must be logged in). 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) user = db.session.get(User, g.current_user_id)
if not user: if not user:
return jsonify({'error': 'User not found'}), 404 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). # Collect existing credential IDs to exclude (prevent re-registering same key).
existing = WebAuthnCredential.query.filter_by(user_id=user.id).all() existing = WebAuthnCredential.query.filter_by(user_id=user.id).all()
exclude_credentials = [ exclude_credentials = [
@@ -102,7 +115,7 @@ def register_begin():
authenticator_selection=AuthenticatorSelectionCriteria( authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.PREFERRED, resident_key=ResidentKeyRequirement.PREFERRED,
user_verification=UserVerificationRequirement.PREFERRED, user_verification=UserVerificationRequirement.PREFERRED,
authenticator_attachment=AuthenticatorAttachment.PLATFORM, authenticator_attachment=authenticator_attachment,
), ),
exclude_credentials=exclude_credentials, exclude_credentials=exclude_credentials,
) )
+18
View File
@@ -2550,3 +2550,21 @@ html.sidebar-open {
.health-banner-dismiss:hover { .health-banner-dismiss:hover {
opacity: 1; 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);
}
+4 -2
View File
@@ -523,9 +523,10 @@ const PasskeyAuth = (() => {
/** /**
* Register a new passkey for the currently logged-in user. * Register a new passkey for the currently logged-in user.
* Requires an active access_token in sessionStorage (set by vault.js on login). * 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) { if (!window.PublicKeyCredential) {
return { error: 'Passkeys are not supported in this browser.' }; return { error: 'Passkeys are not supported in this browser.' };
} }
@@ -540,6 +541,7 @@ const PasskeyAuth = (() => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ attachment }),
}); });
if (!beginRes.ok) { if (!beginRes.ok) {
const d = await beginRes.json().catch(() => ({})); const d = await beginRes.json().catch(() => ({}));
+10 -2
View File
@@ -2442,10 +2442,16 @@ const Vault = (() => {
const lastUsed = c.last_used_at const lastUsed = c.last_used_at
? `Last used ${new Date(c.last_used_at).toLocaleDateString()}` ? `Last used ${new Date(c.last_used_at).toLocaleDateString()}`
: "Never used"; : "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}"> return `<div class="passkey-item" data-cred-id="${c.id}">
<div class="passkey-info"> <div class="passkey-info">
<span class="passkey-name">${escHtml(c.name)}</span> <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>
<div class="passkey-actions"> <div class="passkey-actions">
<button class="btn-text btn-sm btn-rename-passkey" data-id="${c.id}" data-name="${escHtml(c.name)}">Rename</button> <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.dataset.bound = "1";
registerBtn.addEventListener("click", async () => { registerBtn.addEventListener("click", async () => {
const nameInput = document.getElementById("passkey-name-input"); const nameInput = document.getElementById("passkey-name-input");
const attachSel = document.getElementById("passkey-attachment-select");
const name = (nameInput?.value || "").trim() || "Passkey"; const name = (nameInput?.value || "").trim() || "Passkey";
const attachment = attachSel?.value || "platform";
const errEl = document.getElementById("passkeys-error"); const errEl = document.getElementById("passkeys-error");
errEl?.classList.add("hidden"); errEl?.classList.add("hidden");
registerBtn.disabled = true; registerBtn.disabled = true;
registerBtn.textContent = "Waiting…"; registerBtn.textContent = "Waiting…";
const result = await PasskeyAuth.registerPasskey(name); const result = await PasskeyAuth.registerPasskey(name, attachment);
registerBtn.disabled = false; registerBtn.disabled = false;
registerBtn.textContent = "+ Add passkey"; registerBtn.textContent = "+ Add passkey";
+4
View File
@@ -977,6 +977,10 @@
maxlength="128" maxlength="128"
class="passkey-name-input" 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"> <button class="btn-secondary" id="btn-register-passkey">
+ Add passkey + Add passkey
</button> </button>