186 lines
6.9 KiB
HTML
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>
|