auth-yes/ui/components/RecoveryPage.tsx

112 lines
3.8 KiB
TypeScript

import { Layout } from "./Layout.tsx";
export const RecoveryPage = () => {
return (
<Layout title="Account Recovery">
<div
class="card"
style="max-width: 400px; margin: 4rem auto; text-align: center;"
>
<h2>Account Recovery</h2>
<p style="color: #6c757d; margin-bottom: 2rem;">
You have been provided with an out-of-band account recovery link.
Please have your new hardware security key ready.
</p>
<form id="recovery-form">
<input type="hidden" id="recovery-code" name="code" />
<button
type="submit"
class="btn-action btn-success"
style="width: 100%; padding: 0.75rem; font-size: 1rem;"
>
Bind New Passkey
</button>
</form>
<div
id="error-message"
style="color: #dc3545; margin-top: 1rem; display: none;"
>
</div>
<div
id="success-message"
style="color: #28a745; margin-top: 1rem; display: none;"
>
Passkey successfully bound! Redirecting to login...
</div>
</div>
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js">
</script>
<script
dangerouslySetInnerHTML={{
__html: `
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (!code) {
document.getElementById('error-message').textContent = 'No recovery code found in the URL.';
document.getElementById('error-message').style.display = 'block';
document.getElementById('recovery-form').style.display = 'none';
} else {
document.getElementById('recovery-code').value = code;
}
document.getElementById('recovery-form').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = e.target.querySelector('button');
const errorDiv = document.getElementById('error-message');
btn.disabled = true;
btn.textContent = 'Processing...';
errorDiv.style.display = 'none';
try {
const challengeRes = await fetch('/api/recovery/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
if (!challengeRes.ok) {
const data = await challengeRes.json();
throw new Error(data.error || 'Failed to get challenge');
}
const { options } = await challengeRes.json();
const { startRegistration } = SimpleWebAuthnBrowser;
const attResp = await startRegistration({ optionsJSON: options });
const verifyRes = await fetch('/api/recovery/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, response: attResp })
});
if (!verifyRes.ok) {
const data = await verifyRes.json();
throw new Error(data.error || 'Failed to verify passkey');
}
document.getElementById('recovery-form').style.display = 'none';
document.getElementById('success-message').style.display = 'block';
setTimeout(() => {
window.location.href = '/login';
}, 2000);
} catch (err) {
errorDiv.textContent = err.message || 'An error occurred during recovery.';
errorDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Bind New Passkey';
}
});
`,
}}
>
</script>
</Layout>
);
};