function bufferDecode(value) { const padding = "=".repeat((4 - (value.length % 4)) % 4); const base64 = (value + padding).replace(/-/g, "+").replace(/_/g, "/"); const raw = window.atob(base64); return Uint8Array.from([...raw].map((c) => c.charCodeAt(0))); } function bufferEncode(value) { return btoa(String.fromCharCode(...new Uint8Array(value))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } async function registerPasskey() { const nameInput = document.getElementById("passkeyName"); const name = nameInput ? nameInput.value.trim() : "Passkey"; const optionsResp = await fetch("/auth/passkey/register/options", { method: "POST", headers: { "Content-Type": "application/json" }, }); const options = await optionsResp.json(); if (!optionsResp.ok) throw new Error(options.error || "Ошибка passkey"); options.challenge = bufferDecode(options.challenge); options.user.id = bufferDecode(options.user.id); if (options.excludeCredentials) { options.excludeCredentials = options.excludeCredentials.map((item) => ({ ...item, id: bufferDecode(item.id), })); } const credential = await navigator.credentials.create({ publicKey: options }); const verifyResp = await fetch("/auth/passkey/register/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, credential: { id: credential.id, rawId: bufferEncode(credential.rawId), type: credential.type, response: { attestationObject: bufferEncode(credential.response.attestationObject), clientDataJSON: bufferEncode(credential.response.clientDataJSON), }, }, }), }); const result = await verifyResp.json(); if (!verifyResp.ok) throw new Error(result.error || "Не удалось сохранить passkey"); window.location.reload(); } async function loginWithPasskey(username, remember) { const optionsResp = await fetch("/auth/passkey/login/options", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username }), }); const options = await optionsResp.json(); if (!optionsResp.ok) throw new Error(options.error || "Passkey недоступен"); options.challenge = bufferDecode(options.challenge); if (options.allowCredentials) { options.allowCredentials = options.allowCredentials.map((item) => ({ ...item, id: bufferDecode(item.id), })); } const credential = await navigator.credentials.get({ publicKey: options }); const verifyResp = await fetch("/auth/passkey/login/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ remember, credential: { id: credential.id, rawId: bufferEncode(credential.rawId), type: credential.type, response: { authenticatorData: bufferEncode(credential.response.authenticatorData), clientDataJSON: bufferEncode(credential.response.clientDataJSON), signature: bufferEncode(credential.response.signature), userHandle: credential.response.userHandle ? bufferEncode(credential.response.userHandle) : null, }, }, }), }); const result = await verifyResp.json(); if (!verifyResp.ok) throw new Error(result.error || "Ошибка входа"); window.location.href = result.redirect || "/cabinet/"; } document.addEventListener("DOMContentLoaded", () => { const addBtn = document.getElementById("addPasskeyBtn"); if (addBtn) { addBtn.addEventListener("click", async () => { try { if (!window.PublicKeyCredential) { alert("Passkey не поддерживается в этом браузере"); return; } await registerPasskey(); } catch (err) { alert(err.message || "Ошибка passkey"); } }); } const loginBtn = document.getElementById("passkeyLoginBtn"); if (loginBtn) { loginBtn.addEventListener("click", async () => { const loginInput = document.getElementById("login"); const remember = document.querySelector('input[name="remember"]')?.checked; const username = loginInput ? loginInput.value.trim() : ""; if (!username) { alert("Введите логин или email"); return; } try { await loginWithPasskey(username, remember); } catch (err) { alert(err.message || "Ошибка passkey"); } }); } });