/** * This file is part of the SplendidBear Websites' projects. * * Copyright (c) 2026 @ www.splendidbear.org * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import React, { useState, useCallback } from 'react'; import { shape, string } from 'prop-types'; const base64ToArrayBuffer = base64 => { const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/'))); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; }; const arrayBufferToBase64url = buffer => btoa(String.fromCharCode(...new Uint8Array(buffer))) .replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')); const credentialToJSON = credential => ({ id: credential.id, rawId: arrayBufferToBase64url(credential.rawId), type: credential.type, response: { authenticatorData: arrayBufferToBase64url(credential.response.authenticatorData), clientDataJSON: arrayBufferToBase64url(credential.response.clientDataJSON), signature: arrayBufferToBase64url(credential.response.signature), userHandle: credential.response.userHandle ? arrayBufferToBase64url(credential.response.userHandle) : null, }, }); const PasskeyLogin = ({ apiRoutes }) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const handleLogin = useCallback(async () => { setLoading(true); setError(''); try { const beginResponse = await fetch(apiRoutes.authenticationBegin, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); if (!beginResponse.ok) throw new Error('Failed to get authentication options'); const options = await beginResponse.json(); const publicKey = { ...options, challenge: base64ToArrayBuffer(options.challenge), allowCredentials: (options.allowCredentials ?? []).map(cred => ({ ...cred, id: base64ToArrayBuffer(cred.id), })), }; const credential = await navigator.credentials.get({ publicKey }); if (!credential) throw new Error('Authentication was cancelled'); const completeResponse = await fetch(apiRoutes.authenticationComplete, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: credentialToJSON(credential) }), }); if (!completeResponse.ok) { const body = await completeResponse.json(); throw new Error(body.error || 'Authentication failed'); } const result = await completeResponse.json(); if (result.success) { window.location.href = result.redirect || '/'; } } catch (err) { setError(err.message); } setLoading(false); }, [apiRoutes]); return ( <> {error && (
{error}
)} > ); }; export default PasskeyLogin; PasskeyLogin.propTypes = { apiRoutes: shape({ authenticationBegin: string.isRequired, authenticationComplete: string.isRequired, }).isRequired, };