passkey-auth/static/reset.html
2025-07-07 15:00:02 -06:00

186 lines
6.9 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Add Device - Passkey Authentication</title>
<link rel="stylesheet" href="/static/style.css">
<script src="/static/simplewebauthn-browser.min.js"></script>
<script src="/static/qrcodejs/qrcode.min.js"></script>
<script src="/static/awaitable-websocket.js"></script>
</head>
<body>
<div class="container">
<!-- Request Reset View -->
<div id="requestView" class="view">
<h1>🔓 Add Device</h1>
<p>This page is for adding a new device to an existing account. You need a device addition link to proceed.</p>
<div id="requestStatus" class="status info" style="display: block;">
<strong>How to get a device addition link:</strong><br>
1. Log into your account on a device you already have<br>
2. Click "Generate Device Link" in your dashboard<br>
3. Copy the link or scan the QR code to add this device
</div>
<p class="toggle-link" onclick="window.location.href='/'">
Back to Login
</p>
</div>
<!-- Add Passkey View -->
<div id="addPasskeyView" class="view">
<h1>🔑 Add New Passkey</h1>
<div id="userInfo" class="token-info">
<p><strong>Account:</strong> <span id="userName"></span></p>
<p><small>You are about to add a new passkey to this account.</small></p>
</div>
<div id="addPasskeyStatus" class="status"></div>
<button id="addPasskeyBtn" class="btn-primary">Add New Passkey</button>
<p class="toggle-link" onclick="window.location.href='/'">
Back to Login
</p>
</div>
<!-- Success Complete View -->
<div id="completeView" class="view">
<h1>🎉 Passkey Added Successfully!</h1>
<p>Your new passkey has been added to your account. You can now use it to log in.</p>
<button onclick="window.location.href='/'" class="btn-primary">Go to Login</button>
</div>
</div>
<script>
const { startRegistration } = SimpleWebAuthnBrowser;
// Global state
let currentToken = null;
let currentUser = null;
// View management
function showView(viewId) {
document.querySelectorAll('.view').forEach(view => {
view.classList.remove('active');
});
document.getElementById(viewId).classList.add('active');
}
function showAddPasskeyView() {
showView('addPasskeyView');
clearStatus('addPasskeyStatus');
}
function showCompleteView() {
showView('completeView');
}
// Status management
function showStatus(elementId, message, type = 'info') {
const statusEl = document.getElementById(elementId);
statusEl.textContent = message;
statusEl.className = `status ${type}`;
statusEl.style.display = 'block';
}
function clearStatus(elementId) {
const statusEl = document.getElementById(elementId);
statusEl.style.display = 'none';
}
// Validate reset token and show add passkey view
async function validateTokenAndShowAddView(token) {
try {
const response = await fetch('/api/validate-device-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
});
const result = await response.json();
if (result.error) {
throw new Error(result.error);
}
currentToken = token;
currentUser = result;
document.getElementById('userName').textContent = result.user_name;
showAddPasskeyView();
} catch (error) {
showStatus('addPasskeyStatus', `Error: ${error.message}`, 'error');
}
}
// Add new passkey via reset token
async function addPasskeyWithToken(token) {
try {
const ws = await aWebSocket('/ws/add_device_credential');
// Send token to server
ws.send(JSON.stringify({ token }));
// Get registration options
const optionsJSON = JSON.parse(await ws.recv());
if (optionsJSON.error) throw new Error(optionsJSON.error);
showStatus('addPasskeyStatus', 'Save new passkey to your authenticator...', 'info');
const registrationResponse = await startRegistration({ optionsJSON });
ws.send(JSON.stringify(registrationResponse));
const result = JSON.parse(await ws.recv());
if (result.error) throw new Error(`Server: ${result.error}`);
ws.close();
showCompleteView();
} catch (error) {
showStatus('addPasskeyStatus', `Failed to add passkey: ${error.message}`, 'error');
}
}
// Check URL path for token on page load
function checkUrlParams() {
const path = window.location.pathname;
const pathParts = path.split('/');
// Check if URL is in format /reset/token
if (pathParts.length >= 3 && pathParts[1] === 'reset') {
const token = pathParts[2];
if (token) {
validateTokenAndShowAddView(token);
}
}
}
// Form event handlers
document.addEventListener('DOMContentLoaded', function() {
// Check for token in URL
checkUrlParams();
// Add passkey button
const addPasskeyBtn = document.getElementById('addPasskeyBtn');
addPasskeyBtn.addEventListener('click', async () => {
if (!currentToken) {
showStatus('addPasskeyStatus', 'No valid device addition token found', 'error');
return;
}
addPasskeyBtn.disabled = true;
clearStatus('addPasskeyStatus');
try {
showStatus('addPasskeyStatus', 'Starting passkey registration...', 'info');
await addPasskeyWithToken(currentToken);
} catch (err) {
showStatus('addPasskeyStatus', `Registration failed: ${err.message}`, 'error');
} finally {
addPasskeyBtn.disabled = false;
}
});
});
</script>
</body>
</html>