112 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
};
|