/** * 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, { Fragment, useCallback, useEffect, useState } from 'react'; import Dialog from '@mui/material/Dialog'; import { styled } from '@mui/material/styles'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField'; import { arrayOf, shape, string, bool } from 'prop-types'; const StyledDialog = styled(Dialog)({ '& .MuiDialog-paper': { background: '#0a0e14', color: '#e0e0e0', }, '& .MuiTextField-root .MuiInputLabel-root': { color: '#9e9e9e', }, '& .MuiTextField-root .MuiOutlinedInput-root': { '& fieldset': { borderColor: 'rgba(35, 111, 135, 0.5)', }, '&:hover fieldset': { borderColor: 'rgba(35, 111, 135, 0.8)', }, '&.Mui-focused fieldset': { borderColor: '#236f87', }, }, '& .MuiTextField-root .MuiOutlinedInput-input': { color: '#e0e0e0', }, '& .MuiFormHelperText-root': { width: '500px', maxWidth: '94vw', overflow: 'hidden', color: '#e0e0e0', }, '& .MuiBackdrop-root': { background: 'rgba(2, 4, 8, 0.88)', backdropFilter: 'blur(4px)', }, }); 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 credentialToJSON = credential => { const attestationObject = credential.response.attestationObject; const clientDataJSON = credential.response.clientDataJSON; return { id: credential.id, rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')), type: credential.type, response: { attestationObject: btoa(String.fromCharCode(...new Uint8Array(attestationObject))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')), clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(clientDataJSON))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')), }, }; }; const PasskeyManager = ({ credentials, apiRoutes }) => { const [addModalOpen, setAddModalOpen] = useState(false); const [renameModalOpen, setRenameModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [selectedCredential, setSelectedCredential] = useState(null); const [passkeyName, setPasskeyName] = useState(''); const [renameName, setRenameName] = useState(''); const [status, setStatus] = useState({ type: '', message: '' }); const [loading, setLoading] = useState(false); const [credentialsList, setCredentialsList] = useState(credentials); useEffect(() => { setCredentialsList(credentials); }, [credentials]); const showStatus = useCallback((message, type) => { setStatus({ message, type }); }, []); const closeAddModal = useCallback(() => { setAddModalOpen(false); setPasskeyName(''); setStatus({ type: '', message: '' }); }, []); const openAddModal = useCallback(() => { setPasskeyName(''); setStatus({ type: '', message: '' }); setAddModalOpen(true); }, []); const handleAddPasskey = useCallback(async () => { if (!passkeyName.trim()) { showStatus('Please enter a passkey name', 'error'); return; } setLoading(true); showStatus('Starting registration...', 'loading'); try { const optionsResponse = await fetch(apiRoutes.registrationBegin, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credentialName: passkeyName.trim() }), }); if (!optionsResponse.ok) throw new Error('Failed to get registration options'); const options = await optionsResponse.json(); showStatus('Please touch your security key or use biometric authentication...', 'loading'); const publicKey = { ...options, challenge: base64ToArrayBuffer(options.challenge), user: { ...options.user, id: base64ToArrayBuffer(options.user.id), }, attestation: 'direct', }; const credential = await navigator.credentials.create({ publicKey }); if (!credential) { showStatus('Registration was cancelled', 'error'); setLoading(false); return; } showStatus('Verifying credential...', 'loading'); const completeResponse = await fetch(apiRoutes.registrationComplete, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: credentialToJSON(credential), }), }); if (!completeResponse.ok) throw new Error('Failed to complete registration'); showStatus('Passkey registered successfully!', 'success'); setTimeout(() => window.location.reload(), 1500); } catch (error) { console.error('Registration error:', error); showStatus('Error: ' + error.message, 'error'); } setLoading(false); }, [passkeyName, apiRoutes, showStatus]); const openRenameModal = useCallback(credential => { setSelectedCredential(credential); setRenameName(credential.credentialName); setStatus({ type: '', message: '' }); setRenameModalOpen(true); }, []); const closeRenameModal = useCallback(() => { setRenameModalOpen(false); setSelectedCredential(null); setRenameName(''); setStatus({ type: '', message: '' }); }, []); const handleRename = useCallback(async () => { if (!renameName.trim() || !selectedCredential) { showStatus('Please enter a new name', 'error'); return; } setLoading(true); try { const response = await fetch(`${apiRoutes.credentials}/${selectedCredential.id}/rename`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: renameName.trim() }), }); if (!response.ok) throw new Error('Failed to rename passkey'); window.location.reload(); } catch (error) { console.error('Rename error:', error); showStatus('Error: ' + error.message, 'error'); } setLoading(false); }, [renameName, selectedCredential, apiRoutes, showStatus]); const openDeleteModal = useCallback(credential => { setSelectedCredential(credential); setDeleteModalOpen(true); }, []); const closeDeleteModal = useCallback(() => { setDeleteModalOpen(false); setSelectedCredential(null); }, []); const handleDelete = useCallback(async () => { if (!selectedCredential) return; setLoading(true); try { const response = await fetch(`${apiRoutes.credentials}/${selectedCredential.id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); if (response.ok) { window.location.reload(); } else { showStatus('Failed to delete passkey', 'error'); } } catch (error) { console.error('Delete error:', error); showStatus('Error: ' + error.message, 'error'); } setLoading(false); setDeleteModalOpen(false); }, [selectedCredential, apiRoutes, showStatus]); const statusClass = 'error' === status.type ? 'registration-status--error' : 'success' === status.type ? 'registration-status--success' : 'loading' === status.type ? 'registration-status--loading' : ''; return (
{0 < credentialsList.length ? ( credentialsList.map(credential => (

{credential.credentialName}

Created: {credential.createdAt} {credential.lastUsedAt && ( <>
Last used: {credential.lastUsedAt} )}

{credential.isBackupEligible && ( Backup Eligible )} {credential.isBackupAuthenticated && ( Backed Up )}
)) ) : (

No passkeys registered yet.

Add your first passkey to get started with secure, passwordless authentication.

)}
Add New Passkey setPasskeyName(e.target.value)} onKeyUp={e => 'Enter' === e.key && handleAddPasskey()} margin="dense" variant="outlined" helperText="Give this passkey a descriptive name to help you remember it." /> {status.message && (
{status.message}
)}
Rename Passkey setRenameName(e.target.value)} onKeyUp={e => 'Enter' === e.key && handleRename()} margin="dense" variant="outlined" /> {status.message && (
{status.message}
)}
Delete Passkey

Are you sure you want to delete the passkey {selectedCredential && ( {selectedCredential.credentialName}? This action cannot be undone. )}

{status.message && (
{status.message}
)}
); }; export default PasskeyManager; PasskeyManager.propTypes = { credentials: arrayOf(shape({ id: string.isRequired, credentialName: string.isRequired, createdAt: string, lastUsedAt: string, isBackupEligible: bool, isBackupAuthenticated: bool, })).isRequired, apiRoutes: shape({ credentials: string.isRequired, registrationBegin: string.isRequired, registrationComplete: string.isRequired, }).isRequired, };