Refactoring in progress, needs cleanup.
This commit is contained in:
		
							
								
								
									
										2
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.py
									
									
									
									
									
								
							| @@ -191,7 +191,7 @@ async def get_index(): | |||||||
| <html> | <html> | ||||||
| <head> | <head> | ||||||
|     <title>WebAuthn Registration Demo</title> |     <title>WebAuthn Registration Demo</title> | ||||||
|     <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script> |     <script src="/static/simplewebauthn-browser.min.js"></script> | ||||||
|     <style> |     <style> | ||||||
|         body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; } |         body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; } | ||||||
|         .container { text-align: center; } |         .container { text-align: center; } | ||||||
|   | |||||||
| @@ -297,8 +297,42 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") | |||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
| async def get_index(): | async def get_index(): | ||||||
|     """Serve the main HTML page""" |     """Redirect to login page""" | ||||||
|     return FileResponse(STATIC_DIR / "index.html") |     from fastapi.responses import RedirectResponse | ||||||
|  |  | ||||||
|  |     return RedirectResponse(url="/auth/login", status_code=302) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/auth/login") | ||||||
|  | async def get_login_page(): | ||||||
|  |     """Serve the login page""" | ||||||
|  |     return FileResponse(STATIC_DIR / "login.html") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/auth/register") | ||||||
|  | async def get_register_page(): | ||||||
|  |     """Serve the register page""" | ||||||
|  |     return FileResponse(STATIC_DIR / "register.html") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/auth/dashboard") | ||||||
|  | async def get_dashboard_page(): | ||||||
|  |     """Redirect to profile (dashboard is now profile)""" | ||||||
|  |     from fastapi.responses import RedirectResponse | ||||||
|  |  | ||||||
|  |     return RedirectResponse(url="/auth/profile", status_code=302) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/auth/profile") | ||||||
|  | async def get_profile_page(): | ||||||
|  |     """Serve the profile page""" | ||||||
|  |     return FileResponse(STATIC_DIR / "profile.html") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/auth/reset") | ||||||
|  | async def get_reset_page_without_token(): | ||||||
|  |     """Serve the reset page without a token""" | ||||||
|  |     return FileResponse(STATIC_DIR / "reset.html") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/reset/{token}") | @app.get("/reset/{token}") | ||||||
|   | |||||||
							
								
								
									
										93
									
								
								static/add-device.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								static/add-device.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | <!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"> | ||||||
|  |         <!-- Device Addition View --> | ||||||
|  |         <div id="deviceAdditionView" class="view active"> | ||||||
|  |             <h1>📱 Add Device</h1> | ||||||
|  |             <div id="deviceAdditionStatus"></div> | ||||||
|  |              | ||||||
|  |             <div id="deviceLinkSection"> | ||||||
|  |                 <h2>Device Addition Link</h2> | ||||||
|  |                 <div class="token-info"> | ||||||
|  |                     <p><strong>Share this link to add this account to another device:</strong></p> | ||||||
|  |                      | ||||||
|  |                     <div class="qr-container"> | ||||||
|  |                         <div id="qrCode" class="qr-code"></div> | ||||||
|  |                         <p><small>Scan this QR code with your other device</small></p> | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     <div class="link-container"> | ||||||
|  |                         <p class="link-text" id="deviceLinkText">Loading...</p> | ||||||
|  |                         <button class="copy-button" onclick="copyDeviceLink()">Copy Link</button> | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     <p><small>⚠️ This link expires in 24 hours and can only be used once.</small></p> | ||||||
|  |                     <p><strong>Human-readable code:</strong> <code id="deviceToken"></code></p> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |              | ||||||
|  |             <button onclick="window.location.href='/auth/profile'" class="btn-secondary"> | ||||||
|  |                 Back to Profile | ||||||
|  |             </button> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <script src="/static/app.js"></script> | ||||||
|  |     <script> | ||||||
|  |         // Initialize the device addition view when page loads | ||||||
|  |         document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |             initializeApp(); | ||||||
|  |             // Auto-generate device link when page loads | ||||||
|  |             generateDeviceLink(); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Generate device link function | ||||||
|  |         function generateDeviceLink() { | ||||||
|  |             clearStatus('deviceAdditionStatus'); | ||||||
|  |             showStatus('deviceAdditionStatus', 'Generating device link...', 'info'); | ||||||
|  |              | ||||||
|  |             fetch('/api/create-device-link', { | ||||||
|  |                 method: 'POST', | ||||||
|  |                 credentials: 'include' | ||||||
|  |             }) | ||||||
|  |             .then(response => response.json()) | ||||||
|  |             .then(result => { | ||||||
|  |                 if (result.error) throw new Error(result.error); | ||||||
|  |                  | ||||||
|  |                 // Update UI with the link | ||||||
|  |                 document.getElementById('deviceLinkText').textContent = result.addition_link; | ||||||
|  |                 document.getElementById('deviceToken').textContent = result.token; | ||||||
|  |                  | ||||||
|  |                 // Store link globally for copy function | ||||||
|  |                 window.currentDeviceLink = result.addition_link; | ||||||
|  |                  | ||||||
|  |                 // Generate QR code | ||||||
|  |                 const qrCodeEl = document.getElementById('qrCode'); | ||||||
|  |                 qrCodeEl.innerHTML = ''; | ||||||
|  |                 new QRCode(qrCodeEl, { | ||||||
|  |                     text: result.addition_link, | ||||||
|  |                     width: 200, | ||||||
|  |                     height: 200, | ||||||
|  |                     colorDark: '#000000', | ||||||
|  |                     colorLight: '#ffffff', | ||||||
|  |                     correctLevel: QRCode.CorrectLevel.M | ||||||
|  |                 }); | ||||||
|  |                  | ||||||
|  |                 showStatus('deviceAdditionStatus', 'Device link generated successfully!', 'success'); | ||||||
|  |             }) | ||||||
|  |             .catch(error => { | ||||||
|  |                 console.error('Error generating device link:', error); | ||||||
|  |                 showStatus('deviceAdditionStatus', `Failed to generate device link: ${error.message}`, 'error'); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										218
									
								
								static/app.js
									
									
									
									
									
								
							
							
						
						
									
										218
									
								
								static/app.js
									
									
									
									
									
								
							| @@ -51,32 +51,51 @@ async function setSessionCookie(sessionToken) { | |||||||
|  |  | ||||||
| function showView(viewId) { | function showView(viewId) { | ||||||
|   document.querySelectorAll('.view').forEach(view => view.classList.remove('active')) |   document.querySelectorAll('.view').forEach(view => view.classList.remove('active')) | ||||||
|   document.getElementById(viewId).classList.add('active') |   const targetView = document.getElementById(viewId) | ||||||
|  |   if (targetView) { | ||||||
|  |     targetView.classList.add('active') | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function showLoginView() { | function showLoginView() { | ||||||
|  |   if (window.location.pathname !== '/auth/login') { | ||||||
|  |     window.location.href = '/auth/login' | ||||||
|  |     return | ||||||
|  |   } | ||||||
|   showView('loginView') |   showView('loginView') | ||||||
|   clearStatus('loginStatus') |   clearStatus('loginStatus') | ||||||
| } | } | ||||||
|  |  | ||||||
| function showRegisterView() { | function showRegisterView() { | ||||||
|  |   if (window.location.pathname !== '/auth/register') { | ||||||
|  |     window.location.href = '/auth/register' | ||||||
|  |     return | ||||||
|  |   } | ||||||
|   showView('registerView') |   showView('registerView') | ||||||
|   clearStatus('registerStatus') |   clearStatus('registerStatus') | ||||||
| } | } | ||||||
|  |  | ||||||
| function showDeviceAdditionView() { | function showDeviceAdditionView() { | ||||||
|   showView('deviceAdditionView') |   // This function is no longer needed as device addition is now a dialog | ||||||
|   clearStatus('deviceAdditionStatus') |   // Redirect to profile page if someone tries to access the old route | ||||||
|  |   if (window.location.pathname === '/auth/add-device') { | ||||||
|  |     window.location.href = '/auth/profile' | ||||||
|  |     return | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function showDashboardView() { | function showDashboardView() { | ||||||
|   showView('dashboardView') |   if (window.location.pathname !== '/auth/profile') { | ||||||
|   clearStatus('dashboardStatus') |     window.location.href = '/auth/profile' | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   showView('profileView') | ||||||
|  |   clearStatus('profileStatus') | ||||||
|   loadUserInfo().then(() => { |   loadUserInfo().then(() => { | ||||||
|     updateUserInfo() |     updateUserInfo() | ||||||
|     loadCredentials() |     loadCredentials() | ||||||
|   }).catch(error => { |   }).catch(error => { | ||||||
|     showStatus('dashboardStatus', `Failed to load user info: ${error.message}`, 'error') |     showStatus('profileStatus', `Failed to load user info: ${error.message}`, 'error') | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -97,61 +116,6 @@ function clearStatus(elementId) { | |||||||
| // Device Addition & QR Code | // Device Addition & QR Code | ||||||
| // ======================================== | // ======================================== | ||||||
|  |  | ||||||
| async function generateAndShowDeviceLink() { |  | ||||||
|   showView('deviceAdditionView') |  | ||||||
|   clearStatus('deviceAdditionStatus') |  | ||||||
|    |  | ||||||
|   try { |  | ||||||
|     showStatus('deviceAdditionStatus', 'Generating device link...', 'info') |  | ||||||
|      |  | ||||||
|     const response = await fetch('/api/create-device-link', { |  | ||||||
|       method: 'POST', |  | ||||||
|       credentials: 'include' |  | ||||||
|     }) |  | ||||||
|      |  | ||||||
|     const result = await response.json() |  | ||||||
|     if (result.error) throw new Error(result.error) |  | ||||||
|      |  | ||||||
|     // Update UI with the link |  | ||||||
|     document.getElementById('deviceLinkText').textContent = result.addition_link |  | ||||||
|     document.getElementById('deviceToken').textContent = result.token |  | ||||||
|      |  | ||||||
|     // Store link globally for copy function |  | ||||||
|     window.currentDeviceLink = result.addition_link |  | ||||||
|      |  | ||||||
|     // Generate QR code |  | ||||||
|     const qrCodeContainer = document.getElementById('qrCode') |  | ||||||
|     try { |  | ||||||
|       if (typeof QRCode === 'undefined') { |  | ||||||
|         throw new Error('QRCode library not loaded') |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       qrCodeContainer.innerHTML = '' |  | ||||||
|        |  | ||||||
|       new QRCode(qrCodeContainer, { |  | ||||||
|         text: result.addition_link, |  | ||||||
|         width: 200, |  | ||||||
|         height: 200, |  | ||||||
|         colorDark: '#000000', |  | ||||||
|         colorLight: '#ffffff', |  | ||||||
|         correctLevel: QRCode.CorrectLevel.M |  | ||||||
|       }) |  | ||||||
|     } catch (qrError) { |  | ||||||
|       console.error('QR code generation failed:', qrError) |  | ||||||
|       qrCodeContainer.innerHTML = ` |  | ||||||
|         <div style="font-family: monospace; font-size: 12px; line-height: 1; background: white; padding: 10px; border: 1px solid #ccc; display: inline-block;"> |  | ||||||
|           QR Code generation failed. Use the link below instead. |  | ||||||
|         </div> |  | ||||||
|       ` |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     showStatus('deviceAdditionStatus', 'Device link generated successfully!', 'success') |  | ||||||
|      |  | ||||||
|   } catch (error) { |  | ||||||
|     showStatus('deviceAdditionStatus', `Failed to generate device link: ${error.message}`, 'error') |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function copyDeviceLink() { | async function copyDeviceLink() { | ||||||
|   try { |   try { | ||||||
|     if (window.currentDeviceLink) { |     if (window.currentDeviceLink) { | ||||||
| @@ -324,7 +288,8 @@ async function authenticate() { | |||||||
| // Load user credentials | // Load user credentials | ||||||
| async function loadCredentials() { | async function loadCredentials() { | ||||||
|   try { |   try { | ||||||
|     showStatus('dashboardStatus', 'Loading credentials...', 'info') |     const statusElement = document.getElementById('profileStatus') ? 'profileStatus' : 'dashboardStatus' | ||||||
|  |     showStatus(statusElement, 'Loading credentials...', 'info') | ||||||
|      |      | ||||||
|     const response = await fetch('/api/user-credentials', { |     const response = await fetch('/api/user-credentials', { | ||||||
|       method: 'GET', |       method: 'GET', | ||||||
| @@ -337,9 +302,10 @@ async function loadCredentials() { | |||||||
|     currentCredentials = result.credentials |     currentCredentials = result.credentials | ||||||
|     aaguidInfo = result.aaguid_info || {} |     aaguidInfo = result.aaguid_info || {} | ||||||
|     updateCredentialList() |     updateCredentialList() | ||||||
|     clearStatus('dashboardStatus') |     clearStatus(statusElement) | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     showStatus('dashboardStatus', `Failed to load credentials: ${error.message}`, 'error') |     const statusElement = document.getElementById('profileStatus') ? 'profileStatus' : 'dashboardStatus' | ||||||
|  |     showStatus(statusElement, `Failed to load credentials: ${error.message}`, 'error') | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -459,72 +425,108 @@ async function logout() { | |||||||
|   currentUser = null |   currentUser = null | ||||||
|   currentCredentials = [] |   currentCredentials = [] | ||||||
|   aaguidInfo = {} |   aaguidInfo = {} | ||||||
|   showLoginView() |   window.location.href = '/auth/login' | ||||||
| } | } | ||||||
|  |  | ||||||
| // Check if user is already logged in on page load | // Check if user is already logged in on page load | ||||||
| async function checkExistingSession() { | async function checkExistingSession() { | ||||||
|   if (await validateStoredToken()) { |   const isLoggedIn = await validateStoredToken() | ||||||
|     showDashboardView() |   const path = window.location.pathname | ||||||
|  |    | ||||||
|  |   // Protected routes that require authentication | ||||||
|  |   const protectedRoutes = ['/auth/profile'] | ||||||
|  |    | ||||||
|  |   if (isLoggedIn) { | ||||||
|  |     // User is logged in | ||||||
|  |     if (path === '/auth/login' || path === '/auth/register' || path === '/') { | ||||||
|  |       // Redirect to profile if accessing login/register pages while logged in | ||||||
|  |       window.location.href = '/auth/profile' | ||||||
|  |     } else if (path === '/auth/add-device') { | ||||||
|  |       // Redirect old add-device route to profile | ||||||
|  |       window.location.href = '/auth/profile' | ||||||
|  |     } else if (protectedRoutes.includes(path)) { | ||||||
|  |       // Stay on current protected page and load user data | ||||||
|  |       if (path === '/auth/profile') { | ||||||
|  |         loadUserInfo().then(() => { | ||||||
|  |           updateUserInfo() | ||||||
|  |           loadCredentials() | ||||||
|  |         }).catch(error => { | ||||||
|  |           showStatus('profileStatus', `Failed to load user info: ${error.message}`, 'error') | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } else { |   } else { | ||||||
|     showLoginView() |     // User is not logged in | ||||||
|  |     if (protectedRoutes.includes(path) || path === '/auth/add-device') { | ||||||
|  |       // Redirect to login if accessing protected pages without authentication | ||||||
|  |       window.location.href = '/auth/login' | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Initialize the app based on current page | ||||||
|  | function initializeApp() { | ||||||
|  |   checkExistingSession() | ||||||
|  | } | ||||||
|  |  | ||||||
| // Form event handlers | // Form event handlers | ||||||
| document.addEventListener('DOMContentLoaded', function() { | document.addEventListener('DOMContentLoaded', function() { | ||||||
|   // Check for existing session on page load |   // Check for existing session on page load | ||||||
|   checkExistingSession() |   initializeApp() | ||||||
|    |    | ||||||
|   // Registration form |   // Registration form | ||||||
|   const regForm = document.getElementById('registrationForm') |   const regForm = document.getElementById('registrationForm') | ||||||
|   const regSubmitBtn = regForm.querySelector('button[type="submit"]') |   if (regForm) { | ||||||
|  |     const regSubmitBtn = regForm.querySelector('button[type="submit"]') | ||||||
|      |      | ||||||
|   regForm.addEventListener('submit', async (ev) => { |     regForm.addEventListener('submit', async (ev) => { | ||||||
|     ev.preventDefault() |       ev.preventDefault() | ||||||
|     regSubmitBtn.disabled = true |       regSubmitBtn.disabled = true | ||||||
|     clearStatus('registerStatus') |       clearStatus('registerStatus') | ||||||
|        |        | ||||||
|     const user_name = (new FormData(regForm)).get('username') |       const user_name = (new FormData(regForm)).get('username') | ||||||
|        |        | ||||||
|     try { |       try { | ||||||
|       showStatus('registerStatus', 'Starting registration...', 'info') |         showStatus('registerStatus', 'Starting registration...', 'info') | ||||||
|       await register(user_name) |         await register(user_name) | ||||||
|       showStatus('registerStatus', `Registration successful for ${user_name}!`, 'success') |         showStatus('registerStatus', `Registration successful for ${user_name}!`, 'success') | ||||||
|          |          | ||||||
|       // Auto-login after successful registration |         // Auto-login after successful registration | ||||||
|       setTimeout(() => { |         setTimeout(() => { | ||||||
|         showDashboardView() |           window.location.href = '/auth/profile' | ||||||
|       }, 1500) |         }, 1500) | ||||||
|     } catch (err) { |       } catch (err) { | ||||||
|       showStatus('registerStatus', `Registration failed: ${err.message}`, 'error') |         showStatus('registerStatus', `Registration failed: ${err.message}`, 'error') | ||||||
|     } finally { |       } finally { | ||||||
|       regSubmitBtn.disabled = false |         regSubmitBtn.disabled = false | ||||||
|     } |       } | ||||||
|   }) |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Authentication form |   // Authentication form | ||||||
|   const authForm = document.getElementById('authenticationForm') |   const authForm = document.getElementById('authenticationForm') | ||||||
|   const authSubmitBtn = authForm.querySelector('button[type="submit"]') |   if (authForm) { | ||||||
|  |     const authSubmitBtn = authForm.querySelector('button[type="submit"]') | ||||||
|      |      | ||||||
|   authForm.addEventListener('submit', async (ev) => { |     authForm.addEventListener('submit', async (ev) => { | ||||||
|     ev.preventDefault() |       ev.preventDefault() | ||||||
|     authSubmitBtn.disabled = true |       authSubmitBtn.disabled = true | ||||||
|     clearStatus('loginStatus') |       clearStatus('loginStatus') | ||||||
|        |        | ||||||
|     try { |       try { | ||||||
|       showStatus('loginStatus', 'Starting authentication...', 'info') |         showStatus('loginStatus', 'Starting authentication...', 'info') | ||||||
|       await authenticate() |         await authenticate() | ||||||
|       showStatus('loginStatus', 'Authentication successful!', 'success') |         showStatus('loginStatus', 'Authentication successful!', 'success') | ||||||
|          |          | ||||||
|       // Navigate to dashboard |         // Navigate to profile | ||||||
|       setTimeout(() => { |         setTimeout(() => { | ||||||
|         showDashboardView() |           window.location.href = '/auth/profile' | ||||||
|       }, 1000) |         }, 1000) | ||||||
|     } catch (err) { |       } catch (err) { | ||||||
|       showStatus('loginStatus', `Authentication failed: ${err.message}`, 'error') |         showStatus('loginStatus', `Authentication failed: ${err.message}`, 'error') | ||||||
|     } finally { |       } finally { | ||||||
|       authSubmitBtn.disabled = false |         authSubmitBtn.disabled = false | ||||||
|     } |       } | ||||||
|   }) |     }) | ||||||
|  |   } | ||||||
| }) | }) | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								static/dashboard.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								static/dashboard.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <title>Dashboard - 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"> | ||||||
|  |         <!-- Dashboard View --> | ||||||
|  |         <div id="dashboardView" class="view active"> | ||||||
|  |             <h1>👋 Welcome!</h1> | ||||||
|  |             <div id="userInfo" class="user-info"></div> | ||||||
|  |             <div id="dashboardStatus"></div> | ||||||
|  |              | ||||||
|  |             <h2>Your Passkeys</h2> | ||||||
|  |             <div id="credentialList" class="credential-list"> | ||||||
|  |                 <p>Loading credentials...</p> | ||||||
|  |             </div> | ||||||
|  |              | ||||||
|  |             <button onclick="addNewCredential()" class="btn-primary"> | ||||||
|  |                 Add New Passkey | ||||||
|  |             </button> | ||||||
|  |             <button onclick="generateAndShowDeviceLink()" class="btn-secondary"> | ||||||
|  |                 Generate Device Link | ||||||
|  |             </button> | ||||||
|  |             <button onclick="logout()" class="btn-danger"> | ||||||
|  |                 Logout | ||||||
|  |             </button> | ||||||
|  |              | ||||||
|  |             <!-- Device Addition Section --> | ||||||
|  |             <div id="deviceLinkSection" style="display: none;"> | ||||||
|  |                 <h2>Device Addition Link</h2> | ||||||
|  |                 <div class="token-info"> | ||||||
|  |                     <p><strong>Share this link to add this account to another device:</strong></p> | ||||||
|  |                      | ||||||
|  |                     <div class="qr-container"> | ||||||
|  |                         <div id="qrCode" class="qr-code"></div> | ||||||
|  |                         <p><small>Scan this QR code with your other device</small></p> | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     <div class="link-container"> | ||||||
|  |                         <p class="link-text" id="deviceLinkText">Loading...</p> | ||||||
|  |                         <button class="copy-button" onclick="copyDeviceLink()">Copy Link</button> | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     <p><small>⚠️ This link expires in 24 hours and can only be used once.</small></p> | ||||||
|  |                     <p><strong>Human-readable code:</strong> <code id="deviceToken"></code></p> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <script src="/static/app.js"></script> | ||||||
|  |     <script> | ||||||
|  |         // Initialize the dashboard view when page loads | ||||||
|  |         document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |             initializeApp(); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Override the generateAndShowDeviceLink function to show the device link section | ||||||
|  |         function generateAndShowDeviceLink() { | ||||||
|  |             clearStatus('dashboardStatus'); | ||||||
|  |              | ||||||
|  |             fetch('/api/create-device-link', { | ||||||
|  |                 method: 'POST', | ||||||
|  |                 credentials: 'include' | ||||||
|  |             }) | ||||||
|  |             .then(response => response.json()) | ||||||
|  |             .then(result => { | ||||||
|  |                 if (result.error) throw new Error(result.error); | ||||||
|  |                  | ||||||
|  |                 // Update UI with the link | ||||||
|  |                 document.getElementById('deviceLinkText').textContent = result.addition_link; | ||||||
|  |                 document.getElementById('deviceToken').textContent = result.token; | ||||||
|  |                  | ||||||
|  |                 // Store link globally for copy function | ||||||
|  |                 window.currentDeviceLink = result.addition_link; | ||||||
|  |                  | ||||||
|  |                 // Generate QR code | ||||||
|  |                 const qrCodeEl = document.getElementById('qrCode'); | ||||||
|  |                 qrCodeEl.innerHTML = ''; | ||||||
|  |                 new QRCode(qrCodeEl, { | ||||||
|  |                     text: result.addition_link, | ||||||
|  |                     width: 200, | ||||||
|  |                     height: 200, | ||||||
|  |                     colorDark: '#000000', | ||||||
|  |                     colorLight: '#ffffff', | ||||||
|  |                     correctLevel: QRCode.CorrectLevel.M | ||||||
|  |                 }); | ||||||
|  |                  | ||||||
|  |                 // Show the device link section | ||||||
|  |                 document.getElementById('deviceLinkSection').style.display = 'block'; | ||||||
|  |                  | ||||||
|  |                 showStatus('dashboardStatus', 'Device link generated successfully!', 'success'); | ||||||
|  |             }) | ||||||
|  |             .catch(error => { | ||||||
|  |                 console.error('Error generating device link:', error); | ||||||
|  |                 showStatus('dashboardStatus', `Failed to generate device link: ${error.message}`, 'error'); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| <head> | <head> | ||||||
|     <title>Passkey Authentication</title> |     <title>Passkey Authentication</title> | ||||||
|     <link rel="stylesheet" href="/static/style.css"> |     <link rel="stylesheet" href="/static/style.css"> | ||||||
|     <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script> |     <script src="/static/simplewebauthn-browser.min.js"></script> | ||||||
|     <script src="/static/qrcodejs/qrcode.min.js"></script> |     <script src="/static/qrcodejs/qrcode.min.js"></script> | ||||||
|     <script src="/static/awaitable-websocket.js"></script> |     <script src="/static/awaitable-websocket.js"></script> | ||||||
| </head> | </head> | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								static/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								static/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <title>Login - Passkey Authentication</title> | ||||||
|  |     <link rel="stylesheet" href="/static/style.css"> | ||||||
|  |     <script src="/static/simplewebauthn-browser.min.js"></script> | ||||||
|  |     <script src="/static/awaitable-websocket.js"></script> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <!-- Login View --> | ||||||
|  |         <div id="loginView" class="view active"> | ||||||
|  |             <h1>🔐 Passkey Login</h1> | ||||||
|  |             <div id="loginStatus"></div> | ||||||
|  |             <form id="authenticationForm"> | ||||||
|  |                 <button type="submit" class="btn-primary">Login with Your Device</button> | ||||||
|  |             </form> | ||||||
|  |             <p class="toggle-link" onclick="window.location.href='/auth/register'"> | ||||||
|  |                 Don't have an account? Register here | ||||||
|  |             </p> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <script src="/static/app.js"></script> | ||||||
|  |     <script src="/static/util.js"></script> | ||||||
|  |     <script src="/static/login.js"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										33
									
								
								static/login.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								static/login.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | // Login page specific functionality | ||||||
|  |  | ||||||
|  | document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |   // Initialize the app | ||||||
|  |   initializeApp(); | ||||||
|  |    | ||||||
|  |   // Authentication form handler | ||||||
|  |   const authForm = document.getElementById('authenticationForm'); | ||||||
|  |   if (authForm) { | ||||||
|  |     const authSubmitBtn = authForm.querySelector('button[type="submit"]'); | ||||||
|  |      | ||||||
|  |     authForm.addEventListener('submit', async (ev) => { | ||||||
|  |       ev.preventDefault(); | ||||||
|  |       authSubmitBtn.disabled = true; | ||||||
|  |       clearStatus('loginStatus'); | ||||||
|  |        | ||||||
|  |       try { | ||||||
|  |         showStatus('loginStatus', 'Starting authentication...', 'info'); | ||||||
|  |         await authenticate(); | ||||||
|  |         showStatus('loginStatus', 'Authentication successful!', 'success'); | ||||||
|  |          | ||||||
|  |         // Navigate to profile | ||||||
|  |         setTimeout(() => { | ||||||
|  |           window.location.href = '/auth/profile'; | ||||||
|  |         }, 1000); | ||||||
|  |       } catch (err) { | ||||||
|  |         showStatus('loginStatus', `Authentication failed: ${err.message}`, 'error'); | ||||||
|  |       } finally { | ||||||
|  |         authSubmitBtn.disabled = false; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
							
								
								
									
										211
									
								
								static/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								static/profile.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <title>Profile - 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> | ||||||
|  |     <style> | ||||||
|  |         /* Dialog backdrop and blur effects */ | ||||||
|  |         .dialog-backdrop { | ||||||
|  |             position: fixed; | ||||||
|  |             top: 0; | ||||||
|  |             left: 0; | ||||||
|  |             width: 100%; | ||||||
|  |             height: 100%; | ||||||
|  |             background: rgba(0, 0, 0, 0.5); | ||||||
|  |             backdrop-filter: blur(4px); | ||||||
|  |             z-index: 998; | ||||||
|  |             display: none; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .dialog-backdrop.active { | ||||||
|  |             display: block; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .container.dialog-open { | ||||||
|  |             filter: blur(2px); | ||||||
|  |             pointer-events: none; | ||||||
|  |             user-select: none; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         /* Dialog styling */ | ||||||
|  |         #deviceLinkDialog { | ||||||
|  |             position: fixed; | ||||||
|  |             top: 50%; | ||||||
|  |             left: 50%; | ||||||
|  |             transform: translate(-50%, -50%); | ||||||
|  |             z-index: 999; | ||||||
|  |             background: white; | ||||||
|  |             border: none; | ||||||
|  |             border-radius: 8px; | ||||||
|  |             box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); | ||||||
|  |             padding: 2rem; | ||||||
|  |             max-width: 500px; | ||||||
|  |             width: 90%; | ||||||
|  |             max-height: 90vh; | ||||||
|  |             overflow-y: auto; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         #deviceLinkDialog::backdrop { | ||||||
|  |             background: rgba(0, 0, 0, 0.5); | ||||||
|  |             backdrop-filter: blur(4px); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         /* Dark mode dialog styling */ | ||||||
|  |         @media (prefers-color-scheme: dark) { | ||||||
|  |             #deviceLinkDialog { | ||||||
|  |                 background: #1a1a1a; | ||||||
|  |                 color: white; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         /* Prevent scrolling when dialog is open */ | ||||||
|  |         body.dialog-open { | ||||||
|  |             overflow: hidden; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <!-- Profile View --> | ||||||
|  |         <div id="profileView" class="view active"> | ||||||
|  |             <h1>👋 Welcome!</h1> | ||||||
|  |             <div id="userInfo" class="user-info"></div> | ||||||
|  |             <div id="profileStatus"></div> | ||||||
|  |              | ||||||
|  |             <h2>Your Passkeys</h2> | ||||||
|  |             <div id="credentialList" class="credential-list"> | ||||||
|  |                 <p>Loading credentials...</p> | ||||||
|  |             </div> | ||||||
|  |              | ||||||
|  |             <button onclick="addNewCredential()" class="btn-primary"> | ||||||
|  |                 Add New Passkey | ||||||
|  |             </button> | ||||||
|  |             <button onclick="openDeviceLinkDialog()" class="btn-secondary"> | ||||||
|  |                 Generate Device Link | ||||||
|  |             </button> | ||||||
|  |             <button onclick="logout()" class="btn-danger"> | ||||||
|  |                 Logout | ||||||
|  |             </button> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <!-- Device Link Dialog --> | ||||||
|  |         <dialog id="deviceLinkDialog"> | ||||||
|  |             <h1>📱 Add Device</h1> | ||||||
|  |             <div id="deviceAdditionStatus"></div> | ||||||
|  |              | ||||||
|  |             <div id="deviceLinkSection"> | ||||||
|  |                 <h2>Device Addition Link</h2> | ||||||
|  |                 <div class="token-info"> | ||||||
|  |                     <p><strong>Share this link to add this account to another device:</strong></p> | ||||||
|  |                      | ||||||
|  |                     <div class="qr-container"> | ||||||
|  |                         <div id="qrCode" class="qr-code"></div> | ||||||
|  |                         <p><small>Scan this QR code with your other device</small></p> | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     <div class="link-container"> | ||||||
|  |                         <p class="link-text" id="deviceLinkText">Loading...</p> | ||||||
|  |                         <button class="copy-button" onclick="copyDeviceLink()">Copy Link</button> | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     <p><small>⚠️ This link expires in 24 hours and can only be used once.</small></p> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |              | ||||||
|  |             <button onclick="closeDeviceLinkDialog()" class="btn-secondary"> | ||||||
|  |                 Close | ||||||
|  |             </button> | ||||||
|  |         </dialog> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <script src="/static/app.js"></script> | ||||||
|  |     <script> | ||||||
|  |         // Initialize the profile view when page loads | ||||||
|  |         document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |             initializeApp(); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Open device link dialog | ||||||
|  |         function openDeviceLinkDialog() { | ||||||
|  |             const dialog = document.getElementById('deviceLinkDialog'); | ||||||
|  |             const container = document.querySelector('.container'); | ||||||
|  |             const body = document.body; | ||||||
|  |              | ||||||
|  |             // Add blur and disable effects | ||||||
|  |             container.classList.add('dialog-open'); | ||||||
|  |             body.classList.add('dialog-open'); | ||||||
|  |              | ||||||
|  |             dialog.showModal(); | ||||||
|  |             generateDeviceLink(); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Close device link dialog | ||||||
|  |         function closeDeviceLinkDialog() { | ||||||
|  |             const dialog = document.getElementById('deviceLinkDialog'); | ||||||
|  |             const container = document.querySelector('.container'); | ||||||
|  |             const body = document.body; | ||||||
|  |              | ||||||
|  |             // Remove blur and disable effects | ||||||
|  |             container.classList.remove('dialog-open'); | ||||||
|  |             body.classList.remove('dialog-open'); | ||||||
|  |              | ||||||
|  |             dialog.close(); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Generate device link function | ||||||
|  |         function generateDeviceLink() { | ||||||
|  |             clearStatus('deviceAdditionStatus'); | ||||||
|  |             showStatus('deviceAdditionStatus', 'Generating device link...', 'info'); | ||||||
|  |              | ||||||
|  |             fetch('/api/create-device-link', { | ||||||
|  |                 method: 'POST', | ||||||
|  |                 credentials: 'include' | ||||||
|  |             }) | ||||||
|  |             .then(response => response.json()) | ||||||
|  |             .then(result => { | ||||||
|  |                 if (result.error) throw new Error(result.error); | ||||||
|  |                  | ||||||
|  |                 // Update UI with the link | ||||||
|  |                 document.getElementById('deviceLinkText').textContent = result.addition_link; | ||||||
|  |                  | ||||||
|  |                 // Store link globally for copy function | ||||||
|  |                 window.currentDeviceLink = result.addition_link; | ||||||
|  |                  | ||||||
|  |                 // Generate QR code | ||||||
|  |                 const qrCodeEl = document.getElementById('qrCode'); | ||||||
|  |                 qrCodeEl.innerHTML = ''; | ||||||
|  |                 new QRCode(qrCodeEl, { | ||||||
|  |                     text: result.addition_link, | ||||||
|  |                     width: 200, | ||||||
|  |                     height: 200, | ||||||
|  |                     colorDark: '#000000', | ||||||
|  |                     colorLight: '#ffffff', | ||||||
|  |                     correctLevel: QRCode.CorrectLevel.M | ||||||
|  |                 }); | ||||||
|  |                  | ||||||
|  |             }) | ||||||
|  |             .catch(error => { | ||||||
|  |                 console.error('Error generating device link:', error); | ||||||
|  |                 showStatus('deviceAdditionStatus', `Failed to generate device link: ${error.message}`, 'error'); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Close dialog when clicking outside | ||||||
|  |         document.getElementById('deviceLinkDialog').addEventListener('click', function(e) { | ||||||
|  |             if (e.target === this) { | ||||||
|  |                 closeDeviceLinkDialog(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Close dialog when pressing Escape key | ||||||
|  |         document.addEventListener('keydown', function(e) { | ||||||
|  |             if (e.key === 'Escape' && document.getElementById('deviceLinkDialog').open) { | ||||||
|  |                 closeDeviceLinkDialog(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										115
									
								
								static/profile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								static/profile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | // Profile page specific functionality | ||||||
|  |  | ||||||
|  | document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |   // Initialize the app | ||||||
|  |   initializeApp(); | ||||||
|  |    | ||||||
|  |   // Setup dialog event handlers | ||||||
|  |   setupDialogHandlers(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Setup dialog event handlers | ||||||
|  | function setupDialogHandlers() { | ||||||
|  |   // Close dialog when clicking outside | ||||||
|  |   const dialog = document.getElementById('deviceLinkDialog'); | ||||||
|  |   if (dialog) { | ||||||
|  |     dialog.addEventListener('click', function(e) { | ||||||
|  |       if (e.target === this) { | ||||||
|  |         closeDeviceLinkDialog(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Close dialog when pressing Escape key | ||||||
|  |   document.addEventListener('keydown', function(e) { | ||||||
|  |     const dialog = document.getElementById('deviceLinkDialog'); | ||||||
|  |     if (e.key === 'Escape' && dialog && dialog.open) { | ||||||
|  |       closeDeviceLinkDialog(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Open device link dialog | ||||||
|  | function openDeviceLinkDialog() { | ||||||
|  |   const dialog = document.getElementById('deviceLinkDialog'); | ||||||
|  |   const container = document.querySelector('.container'); | ||||||
|  |   const body = document.body; | ||||||
|  |    | ||||||
|  |   if (dialog && container && body) { | ||||||
|  |     // Add blur and disable effects | ||||||
|  |     container.classList.add('dialog-open'); | ||||||
|  |     body.classList.add('dialog-open'); | ||||||
|  |      | ||||||
|  |     dialog.showModal(); | ||||||
|  |     generateDeviceLink(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Close device link dialog | ||||||
|  | function closeDeviceLinkDialog() { | ||||||
|  |   const dialog = document.getElementById('deviceLinkDialog'); | ||||||
|  |   const container = document.querySelector('.container'); | ||||||
|  |   const body = document.body; | ||||||
|  |    | ||||||
|  |   if (dialog && container && body) { | ||||||
|  |     // Remove blur and disable effects | ||||||
|  |     container.classList.remove('dialog-open'); | ||||||
|  |     body.classList.remove('dialog-open'); | ||||||
|  |      | ||||||
|  |     dialog.close(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Generate device link function | ||||||
|  | function generateDeviceLink() { | ||||||
|  |   clearStatus('deviceAdditionStatus'); | ||||||
|  |   showStatus('deviceAdditionStatus', 'Generating device link...', 'info'); | ||||||
|  |    | ||||||
|  |   fetch('/api/create-device-link', { | ||||||
|  |     method: 'POST', | ||||||
|  |     credentials: 'include' | ||||||
|  |   }) | ||||||
|  |   .then(response => response.json()) | ||||||
|  |   .then(result => { | ||||||
|  |     if (result.error) throw new Error(result.error); | ||||||
|  |      | ||||||
|  |     // Update UI with the link | ||||||
|  |     const deviceLinkText = document.getElementById('deviceLinkText'); | ||||||
|  |     const deviceToken = document.getElementById('deviceToken'); | ||||||
|  |      | ||||||
|  |     if (deviceLinkText) { | ||||||
|  |       deviceLinkText.textContent = result.addition_link; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (deviceToken) { | ||||||
|  |       deviceToken.textContent = result.token; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Store link globally for copy function | ||||||
|  |     window.currentDeviceLink = result.addition_link; | ||||||
|  |      | ||||||
|  |     // Generate QR code | ||||||
|  |     const qrCodeEl = document.getElementById('qrCode'); | ||||||
|  |     if (qrCodeEl && typeof QRCode !== 'undefined') { | ||||||
|  |       qrCodeEl.innerHTML = ''; | ||||||
|  |       new QRCode(qrCodeEl, { | ||||||
|  |         text: result.addition_link, | ||||||
|  |         width: 200, | ||||||
|  |         height: 200, | ||||||
|  |         colorDark: '#000000', | ||||||
|  |         colorLight: '#ffffff', | ||||||
|  |         correctLevel: QRCode.CorrectLevel.M | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     showStatus('deviceAdditionStatus', 'Device link generated successfully!', 'success'); | ||||||
|  |   }) | ||||||
|  |   .catch(error => { | ||||||
|  |     console.error('Error generating device link:', error); | ||||||
|  |     showStatus('deviceAdditionStatus', `Failed to generate device link: ${error.message}`, 'error'); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Make functions available globally for onclick handlers | ||||||
|  | window.openDeviceLinkDialog = openDeviceLinkDialog; | ||||||
|  | window.closeDeviceLinkDialog = closeDeviceLinkDialog; | ||||||
							
								
								
									
										29
									
								
								static/register.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								static/register.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <title>Register - Passkey Authentication</title> | ||||||
|  |     <link rel="stylesheet" href="/static/style.css"> | ||||||
|  |     <script src="/static/simplewebauthn-browser.min.js"></script> | ||||||
|  |     <script src="/static/awaitable-websocket.js"></script> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <!-- Register View --> | ||||||
|  |         <div id="registerView" class="view active"> | ||||||
|  |             <h1>🔐 Create Account</h1> | ||||||
|  |             <div id="registerStatus"></div> | ||||||
|  |             <form id="registrationForm"> | ||||||
|  |                 <input type="text" name="username" placeholder="Enter username" required> | ||||||
|  |                 <button type="submit" class="btn-primary">Register Passkey</button> | ||||||
|  |             </form> | ||||||
|  |             <p class="toggle-link" onclick="window.location.href='/auth/login'"> | ||||||
|  |                 Already have an account? Login here | ||||||
|  |             </p> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <script src="/static/app.js"></script> | ||||||
|  |     <script src="/static/util.js"></script> | ||||||
|  |     <script src="/static/register.js"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										35
									
								
								static/register.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								static/register.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | // Register page specific functionality | ||||||
|  |  | ||||||
|  | document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |   // Initialize the app | ||||||
|  |   initializeApp(); | ||||||
|  |    | ||||||
|  |   // Registration form handler | ||||||
|  |   const regForm = document.getElementById('registrationForm'); | ||||||
|  |   if (regForm) { | ||||||
|  |     const regSubmitBtn = regForm.querySelector('button[type="submit"]'); | ||||||
|  |      | ||||||
|  |     regForm.addEventListener('submit', async (ev) => { | ||||||
|  |       ev.preventDefault(); | ||||||
|  |       regSubmitBtn.disabled = true; | ||||||
|  |       clearStatus('registerStatus'); | ||||||
|  |        | ||||||
|  |       const user_name = (new FormData(regForm)).get('username'); | ||||||
|  |        | ||||||
|  |       try { | ||||||
|  |         showStatus('registerStatus', 'Starting registration...', 'info'); | ||||||
|  |         await register(user_name); | ||||||
|  |         showStatus('registerStatus', `Registration successful for ${user_name}!`, 'success'); | ||||||
|  |          | ||||||
|  |         // Auto-login after successful registration | ||||||
|  |         setTimeout(() => { | ||||||
|  |           window.location.href = '/auth/profile'; | ||||||
|  |         }, 1500); | ||||||
|  |       } catch (err) { | ||||||
|  |         showStatus('registerStatus', `Registration failed: ${err.message}`, 'error'); | ||||||
|  |       } finally { | ||||||
|  |         regSubmitBtn.disabled = false; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| <head> | <head> | ||||||
|     <title>Add Device - Passkey Authentication</title> |     <title>Add Device - Passkey Authentication</title> | ||||||
|     <link rel="stylesheet" href="/static/style.css"> |     <link rel="stylesheet" href="/static/style.css"> | ||||||
|     <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script> |     <script src="/static/simplewebauthn-browser.min.js"></script> | ||||||
|     <script src="/static/qrcodejs/qrcode.min.js"></script> |     <script src="/static/qrcodejs/qrcode.min.js"></script> | ||||||
|     <script src="/static/awaitable-websocket.js"></script> |     <script src="/static/awaitable-websocket.js"></script> | ||||||
| </head> | </head> | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								static/simplewebauthn-browser.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								static/simplewebauthn-browser.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										103
									
								
								static/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								static/util.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | // Shared utility functions for all views | ||||||
|  |  | ||||||
|  | // Initialize the app based on current page | ||||||
|  | function initializeApp() { | ||||||
|  |   checkExistingSession(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Show status message | ||||||
|  | function showStatus(elementId, message, type = 'info') { | ||||||
|  |   const statusEl = document.getElementById(elementId); | ||||||
|  |   if (statusEl) { | ||||||
|  |     statusEl.innerHTML = `<div class="status ${type}">${message}</div>`; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Clear status message | ||||||
|  | function clearStatus(elementId) { | ||||||
|  |   const statusEl = document.getElementById(elementId); | ||||||
|  |   if (statusEl) { | ||||||
|  |     statusEl.innerHTML = ''; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Check if user is already logged in on page load | ||||||
|  | async function checkExistingSession() { | ||||||
|  |   const isLoggedIn = await validateStoredToken(); | ||||||
|  |   const path = window.location.pathname; | ||||||
|  |    | ||||||
|  |   // Protected routes that require authentication | ||||||
|  |   const protectedRoutes = ['/auth/profile']; | ||||||
|  |    | ||||||
|  |   if (isLoggedIn) { | ||||||
|  |     // User is logged in | ||||||
|  |     if (path === '/auth/login' || path === '/auth/register' || path === '/') { | ||||||
|  |       // Redirect to profile if accessing login/register pages while logged in | ||||||
|  |       window.location.href = '/auth/profile'; | ||||||
|  |     } else if (path === '/auth/add-device') { | ||||||
|  |       // Redirect old add-device route to profile | ||||||
|  |       window.location.href = '/auth/profile'; | ||||||
|  |     } else if (protectedRoutes.includes(path)) { | ||||||
|  |       // Stay on current protected page and load user data | ||||||
|  |       if (path === '/auth/profile') { | ||||||
|  |         loadUserInfo().then(() => { | ||||||
|  |           updateUserInfo(); | ||||||
|  |           loadCredentials(); | ||||||
|  |         }).catch(error => { | ||||||
|  |           showStatus('profileStatus', `Failed to load user info: ${error.message}`, 'error'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // User is not logged in | ||||||
|  |     if (protectedRoutes.includes(path) || path === '/auth/add-device') { | ||||||
|  |       // Redirect to login if accessing protected pages without authentication | ||||||
|  |       window.location.href = '/auth/login'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Validate stored token | ||||||
|  | async function validateStoredToken() { | ||||||
|  |   try { | ||||||
|  |     const response = await fetch('/api/validate-token', { | ||||||
|  |       method: 'GET', | ||||||
|  |       credentials: 'include' | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     const result = await response.json(); | ||||||
|  |     return result.status === 'success'; | ||||||
|  |   } catch (error) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Copy device link to clipboard | ||||||
|  | async function copyDeviceLink() { | ||||||
|  |   try { | ||||||
|  |     if (window.currentDeviceLink) { | ||||||
|  |       await navigator.clipboard.writeText(window.currentDeviceLink); | ||||||
|  |        | ||||||
|  |       const copyButton = document.querySelector('.copy-button'); | ||||||
|  |       if (copyButton) { | ||||||
|  |         const originalText = copyButton.textContent; | ||||||
|  |         copyButton.textContent = 'Copied!'; | ||||||
|  |         copyButton.style.background = '#28a745'; | ||||||
|  |          | ||||||
|  |         setTimeout(() => { | ||||||
|  |           copyButton.textContent = originalText; | ||||||
|  |           copyButton.style.background = '#28a745'; | ||||||
|  |         }, 2000); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Failed to copy link:', error); | ||||||
|  |     const linkText = document.getElementById('deviceLinkText'); | ||||||
|  |     if (linkText) { | ||||||
|  |       const range = document.createRange(); | ||||||
|  |       range.selectNode(linkText); | ||||||
|  |       window.getSelection().removeAllRanges(); | ||||||
|  |       window.getSelection().addRange(range); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko