Major cleanup, refactoring, device registrations.

This commit is contained in:
Leo Vasanko 2025-07-13 12:40:22 -06:00
parent 5a92c6a25f
commit 58368e2de3
29 changed files with 113 additions and 2551 deletions

View File

@ -16,8 +16,8 @@ from .jwt_manager import refresh_session_token, validate_session_token
from .session_manager import (
clear_session_cookie,
get_current_user,
get_session_token_from_auth_header_or_body,
get_session_token_from_request,
get_session_token_from_bearer,
get_session_token_from_cookie,
set_session_cookie,
)
@ -52,7 +52,7 @@ async def get_user_credentials(request: Request) -> dict:
# Get current session credential ID
current_credential_id = None
session_token = get_session_token_from_request(request)
session_token = get_session_token_from_cookie(request)
if session_token:
token_data = validate_session_token(session_token)
if token_data:
@ -65,7 +65,6 @@ async def get_user_credentials(request: Request) -> dict:
user_aaguids = set()
for cred_id in credential_ids:
try:
stored_cred = await db.get_credential_by_id(cred_id)
# Convert AAGUID to string format
@ -90,9 +89,6 @@ async def get_user_credentials(request: Request) -> dict:
"is_current_session": is_current_session,
}
)
except ValueError:
# Skip invalid credentials
continue
# Get AAGUID information for only the AAGUIDs that the user has
aaguid_manager = get_aaguid_manager()
@ -113,7 +109,7 @@ async def get_user_credentials(request: Request) -> dict:
async def refresh_token(request: Request, response: Response) -> dict:
"""Refresh the session token."""
try:
session_token = get_session_token_from_request(request)
session_token = get_session_token_from_cookie(request)
if not session_token:
return {"error": "No session token found"}
@ -134,7 +130,7 @@ async def refresh_token(request: Request, response: Response) -> dict:
async def validate_token(request: Request) -> dict:
"""Validate a session token and return user info."""
try:
session_token = get_session_token_from_request(request)
session_token = get_session_token_from_cookie(request)
if not session_token:
return {"error": "No session token found"}
@ -165,7 +161,7 @@ async def logout(response: Response) -> dict:
async def set_session(request: Request, response: Response) -> dict:
"""Set session cookie using JWT token from request body or Authorization header."""
try:
session_token = await get_session_token_from_auth_header_or_body(request)
session_token = await get_session_token_from_bearer(request)
if not session_token:
return {"error": "No session token provided"}
@ -219,7 +215,7 @@ async def delete_credential(request: Request) -> dict:
return {"error": "Credential not found"}
# Check if this is the current session credential
session_token = get_session_token_from_request(request)
session_token = get_session_token_from_cookie(request)
if session_token:
token_data = validate_session_token(session_token)
if token_data and token_data.get("credential_id") == credential_id_bytes:

View File

@ -9,14 +9,19 @@ This module provides a simple WebAuthn implementation that:
- Enables true passwordless authentication where users don't need to enter a user_name
"""
import logging
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from uuid import UUID, uuid4
from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi import (
Path as FastAPIPath,
)
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from . import db
from .api_handlers import (
@ -40,7 +45,7 @@ STATIC_DIR = Path(__file__).parent.parent / "static"
passkey = Passkey(
rp_id="localhost",
rp_name="Passkey Auth",
origin="http://localhost:8000",
origin="http://localhost:3000",
)
@ -53,15 +58,12 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Passkey Auth", lifespan=lifespan)
@app.websocket("/ws/new_user_registration")
async def websocket_register_new(ws: WebSocket):
@app.websocket("/auth/ws/register_new")
async def websocket_register_new(ws: WebSocket, user_name: str):
"""Register a new user and with a new passkey credential."""
await ws.accept()
try:
# Data for the new user account
form = await ws.receive_json()
user_id = uuid4()
user_name = form["user_name"]
# WebAuthn registration
credential = await register_chat(ws, user_id, user_name)
@ -86,9 +88,12 @@ async def websocket_register_new(ws: WebSocket):
await ws.send_json({"error": str(e)})
except WebSocketDisconnect:
pass
except Exception:
logging.exception("Internal Server Error")
await ws.send_json({"error": "Internal Server Error"})
@app.websocket("/ws/add_credential")
@app.websocket("/auth/ws/add_credential")
async def websocket_register_add(ws: WebSocket):
"""Register a new credential for an existing user."""
await ws.accept()
@ -108,7 +113,6 @@ async def websocket_register_add(ws: WebSocket):
# WebAuthn registration
credential = await register_chat(ws, user_id, user_name, challenge_ids)
print(f"New credential for user {user_id}: {credential}")
# Store the new credential in the database
await db.create_credential_for_user(credential)
@ -124,24 +128,16 @@ async def websocket_register_add(ws: WebSocket):
await ws.send_json({"error": str(e)})
except WebSocketDisconnect:
pass
except Exception as e:
await ws.send_json({"error": f"Server error: {str(e)}"})
except Exception:
logging.exception("Internal Server Error")
await ws.send_json({"error": "Internal Server Error"})
@app.websocket("/ws/add_device_credential")
async def websocket_add_device_credential(ws: WebSocket):
@app.websocket("/auth/ws/add_device_credential")
async def websocket_add_device_credential(ws: WebSocket, token: str):
"""Add a new credential for an existing user via device addition token."""
await ws.accept()
try:
# Get device addition token from client
message = await ws.receive_json()
token = message.get("token")
if not token:
await ws.send_json({"error": "Device addition token is required"})
return
# Validate device addition token
reset_token = await db.get_reset_token(token)
if not reset_token:
await ws.send_json({"error": "Invalid or expired device addition token"})
@ -182,8 +178,9 @@ async def websocket_add_device_credential(ws: WebSocket):
await ws.send_json({"error": str(e)})
except WebSocketDisconnect:
pass
except Exception as e:
await ws.send_json({"error": f"Server error: {str(e)}"})
except Exception:
logging.exception("Internal Server Error")
await ws.send_json({"error": "Internal Server Error"})
async def register_chat(
@ -200,11 +197,10 @@ async def register_chat(
)
await ws.send_json(options)
response = await ws.receive_json()
print(response)
return passkey.reg_verify(response, challenge, user_id)
@app.websocket("/ws/authenticate")
@app.websocket("/auth/ws/authenticate")
async def websocket_authenticate(ws: WebSocket):
await ws.accept()
try:
@ -231,114 +227,109 @@ async def websocket_authenticate(ws: WebSocket):
"session_token": session_token,
}
)
except ValueError as e:
except (ValueError, InvalidAuthenticationResponse) as e:
logging.exception("ValueError")
await ws.send_json({"error": str(e)})
except WebSocketDisconnect:
pass
except Exception:
logging.exception("Internal Server Error")
await ws.send_json({"error": "Internal Server Error"})
@app.get("/api/user-info")
@app.get("/auth/user-info")
async def api_get_user_info(request: Request):
"""Get user information from session cookie."""
return await get_user_info(request)
@app.get("/api/user-credentials")
@app.get("/auth/user-credentials")
async def api_get_user_credentials(request: Request):
"""Get all credentials for a user using session cookie."""
return await get_user_credentials(request)
@app.post("/api/refresh-token")
@app.post("/auth/refresh-token")
async def api_refresh_token(request: Request, response: Response):
"""Refresh the session token."""
return await refresh_token(request, response)
@app.get("/api/validate-token")
@app.get("/auth/validate-token")
async def api_validate_token(request: Request):
"""Validate a session token and return user info."""
return await validate_token(request)
@app.post("/api/logout")
@app.post("/auth/logout")
async def api_logout(response: Response):
"""Log out the current user by clearing the session cookie."""
return await logout(response)
@app.post("/api/set-session")
@app.post("/auth/set-session")
async def api_set_session(request: Request, response: Response):
"""Set session cookie using JWT token from request body or Authorization header."""
return await set_session(request, response)
@app.post("/api/delete-credential")
@app.post("/auth/delete-credential")
async def api_delete_credential(request: Request):
"""Delete a specific credential for the current user."""
return await delete_credential(request)
@app.post("/api/create-device-link")
@app.post("/auth/create-device-link")
async def api_create_device_link(request: Request):
"""Create a device addition link for the authenticated user."""
return await create_device_addition_link(request)
@app.post("/api/validate-device-token")
@app.post("/auth/validate-device-token")
async def api_validate_device_token(request: Request):
"""Validate a device addition token."""
return await validate_device_addition_token(request)
@app.get("/auth/{passphrase}")
async def reset_authentication(
passphrase: str = FastAPIPath(pattern=r"^\w+(\.\w+){2,}$"),
):
response = RedirectResponse(url="/", status_code=303)
response.set_cookie(
key="auth-token",
value=passphrase,
httponly=False,
secure=True,
samesite="strict",
max_age=2,
)
return response
@app.get("/auth/user-info-by-passphrase")
async def api_get_user_info_by_passphrase(token: str):
"""Get user information using the passphrase."""
reset_token = await db.get_reset_token(token)
if not reset_token:
return Response(content="Invalid or expired passphrase", status_code=403)
user = await db.get_user_by_id(reset_token.user_id)
if not user:
return Response(content="User not found", status_code=404)
return {"user_name": user.user_name}
# Serve static files
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
@app.get("/")
async def get_index():
"""Redirect to login page"""
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}")
async def get_reset_page(token: str):
"""Serve the reset page with the token in URL"""
return FileResponse(STATIC_DIR / "reset.html")
# Catch-all route for SPA - serve index.html for all non-API routes
@app.get("/{path:path}")
async def spa_handler(path: str):
"""Serve the Vue SPA for all routes (except API and static)"""
return FileResponse(STATIC_DIR / "index.html")
def main():
@ -355,4 +346,5 @@ def main():
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
main()

View File

@ -60,7 +60,7 @@ class Passkey:
self,
rp_id: str,
rp_name: str,
origin: str,
origin: str | None = None,
supported_pub_key_algs: list[COSEAlgorithmIdentifier] | None = None,
):
"""
@ -74,7 +74,7 @@ class Passkey:
"""
self.rp_id = rp_id
self.rp_name = rp_name
self.origin = origin
self.origin = origin or f"https://{rp_id}"
self.supported_pub_key_algs = supported_pub_key_algs or [
COSEAlgorithmIdentifier.EDDSA,
COSEAlgorithmIdentifier.ECDSA_SHA_256,

View File

@ -25,19 +25,18 @@ async def create_device_addition_link(request: Request) -> dict:
return {"error": "Authentication required"}
# Generate a human-readable token
token = generate(n=4, sep="-") # e.g., "able-ocean-forest-dawn"
token = generate(n=4, sep=".") # e.g., "able-ocean-forest-dawn"
# Create reset token in database
await db.create_reset_token(user.user_id, token)
# Generate the device addition link with pretty URL
addition_link = f"http://localhost:8000/reset/{token}"
addition_link = f"{request.headers.get('origin', '')}/auth/{token}"
return {
"status": "success",
"message": "Device addition link generated successfully",
"addition_link": addition_link,
"token": token,
"expires_in_hours": 24,
}

View File

@ -7,7 +7,6 @@ This module provides session management functionality including:
- Session validation and token handling
"""
from typing import Optional
from uuid import UUID
from fastapi import Request, Response
@ -15,11 +14,11 @@ from fastapi import Request, Response
from .db import User, get_user_by_id
from .jwt_manager import validate_session_token
COOKIE_NAME = "session_token"
COOKIE_NAME = "auth"
COOKIE_MAX_AGE = 86400 # 24 hours
async def get_current_user(request: Request) -> Optional[User]:
async def get_current_user(request: Request) -> User | None:
"""Get the current user from the session cookie."""
session_token = request.cookies.get(COOKIE_NAME)
if not session_token:
@ -43,7 +42,7 @@ def set_session_cookie(response: Response, session_token: str) -> None:
value=session_token,
max_age=COOKIE_MAX_AGE,
httponly=True,
secure=False, # Set to True in production with HTTPS
secure=True,
samesite="lax",
)
@ -53,36 +52,29 @@ def clear_session_cookie(response: Response) -> None:
response.delete_cookie(key=COOKIE_NAME)
def get_session_token_from_request(request: Request) -> Optional[str]:
def get_session_token_from_cookie(request: Request) -> str | None:
"""Extract session token from request cookies."""
return request.cookies.get(COOKIE_NAME)
async def validate_session_from_request(request: Request) -> Optional[dict]:
async def validate_session_from_request(request: Request) -> dict | None:
"""Validate session token from request and return token data."""
session_token = get_session_token_from_request(request)
session_token = get_session_token_from_cookie(request)
if not session_token:
return None
return validate_session_token(session_token)
async def get_session_token_from_auth_header_or_body(request: Request) -> Optional[str]:
async def get_session_token_from_bearer(request: Request) -> str | None:
"""Extract session token from Authorization header or request body."""
# Try to get token from Authorization header first
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
return auth_header[7:] # Remove "Bearer " prefix
# Try to get from request body
try:
body = await request.json()
return body.get("session_token")
except Exception:
return None
return auth_header.removeprefix("Bearer ")
async def get_user_from_cookie_string(cookie_header: str) -> Optional[UUID]:
async def get_user_from_cookie_string(cookie_header: str) -> UUID | None:
"""Parse cookie header and return user ID if valid session exists."""
if not cookie_header:
return None

View File

@ -1,475 +0,0 @@
const { startRegistration, startAuthentication } = SimpleWebAuthnBrowser
// Global state
let currentUser = null
let currentCredentials = []
let aaguidInfo = {}
// ========================================
// Session Management
// ========================================
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
}
}
async function setSessionCookie(sessionToken) {
try {
const response = await fetch('/api/set-session', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sessionToken}`,
'Content-Type': 'application/json'
},
credentials: 'include'
})
const result = await response.json()
if (result.error) {
throw new Error(result.error)
}
return result
} catch (error) {
throw new Error(`Failed to set session cookie: ${error.message}`)
}
}
// ========================================
// View Management
// ========================================
function showView(viewId) {
document.querySelectorAll('.view').forEach(view => view.classList.remove('active'))
const targetView = document.getElementById(viewId)
if (targetView) {
targetView.classList.add('active')
}
}
function showLoginView() {
if (window.location.pathname !== '/auth/login') {
window.location.href = '/auth/login'
return
}
showView('loginView')
clearStatus('loginStatus')
}
function showRegisterView() {
if (window.location.pathname !== '/auth/register') {
window.location.href = '/auth/register'
return
}
showView('registerView')
clearStatus('registerStatus')
}
function showDeviceAdditionView() {
// This function is no longer needed as device addition is now a dialog
// 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
}
}
async function showDashboardView() {
if (window.location.pathname !== '/auth/profile') {
window.location.href = '/auth/profile'
return
}
showView('profileView')
clearStatus('profileStatus')
try {
await loadUserInfo()
updateUserInfo()
await loadCredentials()
} catch (error) {
showStatus('profileStatus', `Failed to load user info: ${error.message}`, 'error')
}
}
// ========================================
// Status Management
// ========================================
function showStatus(elementId, message, type = 'info') {
const statusEl = document.getElementById(elementId)
statusEl.innerHTML = `<div class="status ${type}">${message}</div>`
}
function clearStatus(elementId) {
document.getElementById(elementId).innerHTML = ''
}
// ========================================
// Device Addition & QR Code
// ========================================
async function copyDeviceLink() {
try {
if (window.currentDeviceLink) {
await navigator.clipboard.writeText(window.currentDeviceLink)
const copyButton = document.querySelector('.copy-button')
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')
const range = document.createRange()
range.selectNode(linkText)
window.getSelection().removeAllRanges()
window.getSelection().addRange(range)
}
}
// ========================================
// WebAuthn Operations
// ========================================
async function register(user_name) {
const ws = await aWebSocket('/ws/new_user_registration')
ws.send(JSON.stringify({ user_name }))
const optionsJSON = JSON.parse(await ws.recv())
if (optionsJSON.error) throw new Error(optionsJSON.error)
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}`)
await setSessionCookie(result.session_token)
ws.close()
}
async function authenticate() {
const ws = await aWebSocket('/ws/authenticate')
const optionsJSON = JSON.parse(await ws.recv())
if (optionsJSON.error) throw new Error(optionsJSON.error)
const authenticationResponse = await startAuthentication({ optionsJSON })
ws.send(JSON.stringify(authenticationResponse))
const result = JSON.parse(await ws.recv())
if (result.error) throw new Error(`Server: ${result.error}`)
await setSessionCookie(result.session_token)
ws.close()
}
async function addNewCredential() {
try {
showStatus('dashboardStatus', 'Adding new passkey...', 'info')
const ws = await aWebSocket('/ws/add_credential')
const optionsJSON = JSON.parse(await ws.recv())
if (optionsJSON.error) throw new Error(optionsJSON.error)
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()
showStatus('dashboardStatus', 'New passkey added successfully!', 'success')
setTimeout(() => {
loadCredentials()
clearStatus('dashboardStatus')
}, 2000)
} catch (error) {
showStatus('dashboardStatus', `Failed to add passkey: ${error.message}`, 'error')
}
}
// ========================================
// User Data Management
// ========================================
// User registration
async function register(user_name) {
try {
const ws = await aWebSocket('/ws/new_user_registration')
ws.send(JSON.stringify({user_name}))
// Registration chat
const optionsJSON = JSON.parse(await ws.recv())
if (optionsJSON.error) throw new Error(optionsJSON.error)
showStatus('registerStatus', 'Save 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()
// Set session cookie using the JWT token
await setSessionCookie(result.session_token)
// Set current user from registration result
currentUser = {
user_id: result.user_id,
user_name: user_name,
last_seen: new Date().toISOString()
}
return result
} catch (error) {
throw error
}
}
// User authentication
async function authenticate() {
try {
const ws = await aWebSocket('/ws/authenticate')
const optionsJSON = JSON.parse(await ws.recv())
if (optionsJSON.error) throw new Error(optionsJSON.error)
showStatus('loginStatus', 'Please use your authenticator...', 'info')
const authResponse = await startAuthentication({optionsJSON})
await ws.send(JSON.stringify(authResponse))
const result = JSON.parse(await ws.recv())
if (result.error) throw new Error(`Server: ${result.error}`)
ws.close()
// Set session cookie using the JWT token
await setSessionCookie(result.session_token)
// Authentication successful, now get user info using HTTP endpoint
const userResponse = await fetch('/api/user-info', {
method: 'GET',
credentials: 'include'
})
const userInfo = await userResponse.json()
if (userInfo.error) throw new Error(`Server: ${userInfo.error}`)
currentUser = userInfo.user
return result
} catch (error) {
throw error
}
}
// Load user credentials
async function loadCredentials() {
try {
const statusElement = document.getElementById('profileStatus') ? 'profileStatus' : 'dashboardStatus'
showStatus(statusElement, 'Loading credentials...', 'info')
const response = await fetch('/api/user-credentials', {
method: 'GET',
credentials: 'include'
})
const result = await response.json()
if (result.error) throw new Error(`Server: ${result.error}`)
currentCredentials = result.credentials
aaguidInfo = result.aaguid_info || {}
updateCredentialList()
clearStatus(statusElement)
} catch (error) {
const statusElement = document.getElementById('profileStatus') ? 'profileStatus' : 'dashboardStatus'
showStatus(statusElement, `Failed to load credentials: ${error.message}`, 'error')
}
}
// Load user info using HTTP endpoint
async function loadUserInfo() {
try {
const response = await fetch('/api/user-info', {
method: 'GET',
credentials: 'include'
})
const result = await response.json()
if (result.error) throw new Error(`Server: ${result.error}`)
currentUser = result.user
} catch (error) {
throw error
}
}
// Update user info display
function updateUserInfo() {
const userInfoEl = document.getElementById('userInfo')
if (currentUser) {
userInfoEl.innerHTML = `
<h3>👤 ${currentUser.user_name}</h3>
<p><strong>Visits:</strong> ${currentUser.visits || 0}</p>
<p><strong>Member since:</strong> ${currentUser.created_at ? formatHumanReadableDate(currentUser.created_at) : 'N/A'}</p>
<p><strong>Last seen:</strong> ${currentUser.last_seen ? formatHumanReadableDate(currentUser.last_seen) : 'N/A'}</p>
`
}
}
// Update credential list display
function updateCredentialList() {
const credentialListEl = document.getElementById('credentialList')
if (currentCredentials.length === 0) {
credentialListEl.innerHTML = '<p>No passkeys found.</p>'
return
}
credentialListEl.innerHTML = currentCredentials.map(cred => {
// Get authenticator information from AAGUID
const authInfo = aaguidInfo[cred.aaguid]
const authName = authInfo ? authInfo.name : 'Unknown Authenticator'
// Determine which icon to use based on current theme (you can implement theme detection)
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const iconKey = isDarkMode ? 'icon_dark' : 'icon_light'
const authIcon = authInfo && authInfo[iconKey] ? authInfo[iconKey] : null
// Check if this is the current session credential
const isCurrentSession = cred.is_current_session || false
return `
<div class="credential-item${isCurrentSession ? ' current-session' : ''}">
<div class="credential-header">
<div class="credential-icon">
${authIcon ? `<img src="${authIcon}" alt="${authName}" class="auth-icon" width="32" height="32">` : '<span class="auth-emoji">🔑</span>'}
</div>
<div class="credential-info">
<h4>${authName}</h4>
</div>
<div class="credential-dates">
<span class="date-label">Created:</span>
<span class="date-value">${formatHumanReadableDate(cred.created_at)}</span>
<span class="date-label">Last used:</span>
<span class="date-value">${formatHumanReadableDate(cred.last_used)}</span>
</div>
<div class="credential-actions">
<button onclick="deleteCredential('${cred.credential_id}')"
class="btn-delete-credential"
${isCurrentSession ? 'disabled title="Cannot delete current session credential"' : ''}>
🗑
</button>
</div>
</div>
</div>
`
}).join('')
}
// Helper function to format dates in a human-readable way
function formatHumanReadableDate(dateString) {
if (!dateString) return 'Never'
const date = new Date(dateString)
const now = new Date()
const diffMs = now - date
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffHours < 1) {
return 'Just now'
} else if (diffHours < 24) {
return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
} else if (diffDays <= 7) {
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`
} else {
// For dates older than 7 days, show just the date without time
return date.toLocaleDateString()
}
}
// Logout
async function logout() {
try {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
})
} catch (error) {
console.error('Logout error:', error)
}
currentUser = null
currentCredentials = []
aaguidInfo = {}
window.location.href = '/auth/login'
}
// 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'
}
}
}
// Initialize the app based on current page
function initializeApp() {
checkExistingSession()
}
// Form event handlers
document.addEventListener('DOMContentLoaded', initializeApp)

View File

@ -1,43 +0,0 @@
class AwaitableWebSocket extends WebSocket {
#received = []
#waiting = []
#err = null
#opened = false
constructor(resolve, reject, url, protocols) {
super(url, protocols)
this.onopen = () => {
this.#opened = true
resolve(this)
}
this.onmessage = e => {
if (this.#waiting.length) this.#waiting.shift().resolve(e.data)
else this.#received.push(e.data)
}
this.onclose = e => {
if (!this.#opened) {
reject(new Error(`WebSocket ${this.url} failed to connect, code ${e.code}`))
return
}
this.#err = e.wasClean
? new Error(`Websocket ${this.url} closed ${e.code}`)
: new Error(`WebSocket ${this.url} closed with error ${e.code}`)
this.#waiting.splice(0).forEach(p => p.reject(this.#err))
}
}
recv() {
// If we have a message already received, return it immediately
if (this.#received.length) return Promise.resolve(this.#received.shift())
// Wait for incoming messages, if we have an error, reject immediately
if (this.#err) return Promise.reject(this.#err)
return new Promise((resolve, reject) => this.#waiting.push({ resolve, reject }))
}
}
// Construct an async WebSocket with await aWebSocket(url)
function aWebSocket(url, protocols) {
return new Promise((resolve, reject) => {
new AwaitableWebSocket(resolve, reject, url, protocols)
})
}

View File

@ -1,92 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>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">
<!-- 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="showRegisterView()">
Don't have an account? Register here
</p>
</div>
<!-- Register View -->
<div id="registerView" class="view">
<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="showLoginView()">
Already have an account? Login here
</p>
</div>
<!-- Dashboard View -->
<div id="dashboardView" class="view">
<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>
</div>
<!-- Device Addition View -->
<div id="deviceAdditionView" class="view">
<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="showDashboardView()" class="btn-secondary">
Back to Dashboard
</button>
</div>
</div>
<script src="static/app.js"></script>
</body>
</html>

View File

@ -1,28 +0,0 @@
<!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>

View File

@ -1,38 +0,0 @@
// 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) {
console.error('Login error:', err)
if (err.name === "NotAllowedError") {
showStatus('loginStatus', `Login cancelled`, 'error')
} else {
showStatus('loginStatus', `Login failed: ${err.message}`, 'error')
}
} finally {
authSubmitBtn.disabled = false
}
})
}
})

View File

@ -1,36 +0,0 @@
/* Profile page dialog styles */
.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;
color: black;
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);
}
/* Prevent scrolling when dialog is open */
body.dialog-open {
overflow: hidden;
}

View File

@ -1,64 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Profile - Passkey Authentication</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/profile-dialog.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">
<!-- 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">
<div class="qr-container">
<div id="qrCode" class="qr-code"></div>
<p><a href="#" id="deviceLinkText"></a></p>
</div>
<p>
<strong>Scan the above code and visit the URL on another device.</strong><br>
<small>⚠️ 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 src="/static/profile.js"></script>
</body>
</html>

View File

@ -1,124 +0,0 @@
// 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
async function generateDeviceLink() {
clearStatus('deviceAdditionStatus')
showStatus('deviceAdditionStatus', 'Generating device link...', 'info')
try {
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
const deviceLinkText = document.getElementById('deviceLinkText')
if (deviceLinkText) {
deviceLinkText.href = result.addition_link
deviceLinkText.textContent = result.addition_link.replace(/^[a-z]+:\/\//i, '')
// Add click event listener for copying the link
deviceLinkText.addEventListener('click', function(e) {
e.preventDefault() // Prevent navigation
navigator.clipboard.writeText(deviceLinkText.href).then(() => {
closeDeviceLinkDialog() // Close the dialog
showStatus('deviceAdditionStatus', 'Device registration link copied', 'success') // Display status
}).catch(() => {
showStatus('deviceAdditionStatus', 'Failed to copy device registration link', 'error')
})
})
}
// 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
})
}
clearStatus('deviceAdditionStatus')
} catch (error) {
showStatus('deviceAdditionStatus', `Failed to generate device link: ${error.message}`, 'error')
}
}
// Make functions available globally for onclick handlers
window.openDeviceLinkDialog = openDeviceLinkDialog
window.closeDeviceLinkDialog = closeDeviceLinkDialog

View File

@ -1,4 +0,0 @@
.DS_Store
.idea
.project

View File

@ -1,14 +0,0 @@
The MIT License (MIT)
---------------------
Copyright (c) 2012 davidshimjs
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,46 +0,0 @@
# QRCode.js
QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM.
QRCode.js has no dependencies.
## Basic Usages
```
<div id="qrcode"></div>
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"), "http://jindo.dev.naver.com/collie");
</script>
```
or with some options
```
<div id="qrcode"></div>
<script type="text/javascript">
var qrcode = new QRCode(document.getElementById("qrcode"), {
text: "http://jindo.dev.naver.com/collie",
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
</script>
```
and you can use some methods
```
qrcode.clear(); // clear the code.
qrcode.makeCode("http://naver.com"); // make another code.
```
## Browser Compatibility
IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC.
## License
MIT License
## Contact
twitter @davidshimjs
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

View File

@ -1,18 +0,0 @@
{
"name": "qrcode.js",
"version": "0.0.1",
"homepage": "https://github.com/davidshimjs/qrcodejs",
"authors": [
"Sangmin Shim", "Sangmin Shim <ssm0123@gmail.com> (http://jaguarjs.com)"
],
"description": "Cross-browser QRCode generator for javascript",
"main": "qrcode.js",
"ignore": [
"bower_components",
"node_modules",
"index.html",
"index.svg",
"jquery.min.js",
"qrcode.min.js"
]
}

View File

@ -1,47 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko" lang="ko">
<head>
<title>Cross-Browser QRCode generator for Javascript</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="qrcode.js"></script>
</head>
<body>
<input id="text" type="text" value="http://jindo.dev.naver.com/collie" style="width:80%" />
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="qrcode"/>
</svg>
<script type="text/javascript">
var qrcode = new QRCode(document.getElementById("qrcode"), {
width : 100,
height : 100,
useSVG: true
});
function makeCode () {
var elText = document.getElementById("text");
if (!elText.value) {
alert("Input a text");
elText.focus();
return;
}
qrcode.makeCode(elText.value);
}
makeCode();
$("#text").
on("blur", function () {
makeCode();
}).
on("keydown", function (e) {
if (e.keyCode == 13) {
makeCode();
}
});
</script>
</body>
</html>

View File

@ -1,44 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko" lang="ko">
<head>
<title>Cross-Browser QRCode generator for Javascript</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="qrcode.js"></script>
</head>
<body>
<input id="text" type="text" value="http://jindo.dev.naver.com/collie" style="width:80%" /><br />
<div id="qrcode" style="width:100px; height:100px; margin-top:15px;"></div>
<script type="text/javascript">
var qrcode = new QRCode(document.getElementById("qrcode"), {
width : 100,
height : 100
});
function makeCode () {
var elText = document.getElementById("text");
if (!elText.value) {
alert("Input a text");
elText.focus();
return;
}
qrcode.makeCode(elText.value);
}
makeCode();
$("#text").
on("blur", function () {
makeCode();
}).
on("keydown", function (e) {
if (e.keyCode == 13) {
makeCode();
}
});
</script>
</body>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-50 0 200 100">
<g id="qrcode"/>
<foreignObject x="-50" y="0" width="100" height="100">
<body xmlns="http://www.w3.org/1999/xhtml" style="padding:0; margin:0">
<div style="padding:inherit; margin:inherit; height:100%">
<textarea id="text" style="height:100%; width:100%; position:absolute; margin:inherit; padding:inherit">james</textarea>
</div>
<script type="application/ecmascript" src="qrcode.js"></script>
<script type="application/ecmascript">
var elem = document.getElementById("qrcode");
var qrcode = new QRCode(elem, {
width : 100,
height : 100
});
function makeCode () {
var elText = document.getElementById("text");
if (elText.value === "") {
//alert("Input a text");
//elText.focus();
return;
}
qrcode.makeCode(elText.value);
}
makeCode();
document.getElementById("text").onkeyup = function (e) {
makeCode();
};
</script>
</body>
</foreignObject>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,614 +0,0 @@
/**
* @fileoverview
* - Using the 'QRCode for Javascript library'
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
* - this library has no dependencies.
*
* @author davidshimjs
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
var QRCode;
(function () {
//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
// http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE;
this.data = data;
this.parsedData = [];
// Added to support UTF-8 Characters
for (var i = 0, l = this.data.length; i < l; i++) {
var byteArray = [];
var code = this.data.charCodeAt(i);
if (code > 0x10000) {
byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[3] = 0x80 | (code & 0x3F);
} else if (code > 0x800) {
byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[2] = 0x80 | (code & 0x3F);
} else if (code > 0x80) {
byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
byteArray[1] = 0x80 | (code & 0x3F);
} else {
byteArray[0] = code;
}
this.parsedData.push(byteArray);
}
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
if (this.parsedData.length != this.data.length) {
this.parsedData.unshift(191);
this.parsedData.unshift(187);
this.parsedData.unshift(239);
}
}
QR8bitByte.prototype = {
getLength: function (buffer) {
return this.parsedData.length;
},
write: function (buffer) {
for (var i = 0, l = this.parsedData.length; i < l; i++) {
buffer.put(this.parsedData[i], 8);
}
}
};
function QRCodeModel(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
+buffer.getLengthInBits()
+">"
+totalDataCount*8
+")");}
if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD1,8);}
return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=256){n-=255;}
return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
function _isSupportCanvas() {
return typeof CanvasRenderingContext2D != "undefined";
}
// android 2.x doesn't support Data-URI spec
function _getAndroid() {
var android = false;
var sAgent = navigator.userAgent;
if (/android/i.test(sAgent)) { // android
android = true;
var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
if (aMat && aMat[1]) {
android = parseFloat(aMat[1]);
}
}
return android;
}
var svgDrawer = (function() {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
this.clear();
function makeSVG(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
return el;
}
var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
_el.appendChild(svg);
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
if (oQRCode.isDark(row, col)) {
var child = makeSVG("use", {"x": String(col), "y": String(row)});
child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
svg.appendChild(child);
}
}
}
};
Drawing.prototype.clear = function () {
while (this._el.hasChildNodes())
this._el.removeChild(this._el.lastChild);
};
return Drawing;
})();
var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
// Drawing in DOM by using Table tag
var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_el.innerHTML = aHTML.join('');
// Fix the margin values as real size.
var elTable = _el.childNodes[0];
var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";
}
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._el.innerHTML = '';
};
return Drawing;
})() : (function () { // Drawing in Canvas
function _onMakeImage() {
this._elImage.src = this._elCanvas.toDataURL("image/png");
this._elImage.style.display = "block";
this._elCanvas.style.display = "none";
}
// Android 2.1 bug workaround
// http://code.google.com/p/android/issues/detail?id=5141
if (this._android && this._android <= 2.1) {
var factor = 1 / window.devicePixelRatio;
var drawImage = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (("nodeName" in image) && /img/i.test(image.nodeName)) {
for (var i = arguments.length - 1; i >= 1; i--) {
arguments[i] = arguments[i] * factor;
}
} else if (typeof dw == "undefined") {
arguments[1] *= factor;
arguments[2] *= factor;
arguments[3] *= factor;
arguments[4] *= factor;
}
drawImage.apply(this, arguments);
};
}
/**
* Check whether the user's browser supports Data URI or not
*
* @private
* @param {Function} fSuccess Occurs if it supports Data URI
* @param {Function} fFail Occurs if it doesn't support Data URI
*/
function _safeSetDataURI(fSuccess, fFail) {
var self = this;
self._fFail = fFail;
self._fSuccess = fSuccess;
// Check it just once
if (self._bSupportDataURI === null) {
var el = document.createElement("img");
var fOnError = function() {
self._bSupportDataURI = false;
if (self._fFail) {
self._fFail.call(self);
}
};
var fOnSuccess = function() {
self._bSupportDataURI = true;
if (self._fSuccess) {
self._fSuccess.call(self);
}
};
el.onabort = fOnError;
el.onerror = fOnError;
el.onload = fOnSuccess;
el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
return;
} else if (self._bSupportDataURI === true && self._fSuccess) {
self._fSuccess.call(self);
} else if (self._bSupportDataURI === false && self._fFail) {
self._fFail.call(self);
}
};
/**
* Drawing QRCode by using canvas
*
* @constructor
* @param {HTMLElement} el
* @param {Object} htOption QRCode Options
*/
var Drawing = function (el, htOption) {
this._bIsPainted = false;
this._android = _getAndroid();
this._htOption = htOption;
this._elCanvas = document.createElement("canvas");
this._elCanvas.width = htOption.width;
this._elCanvas.height = htOption.height;
el.appendChild(this._elCanvas);
this._el = el;
this._oContext = this._elCanvas.getContext("2d");
this._bIsPainted = false;
this._elImage = document.createElement("img");
this._elImage.alt = "Scan me!";
this._elImage.style.display = "none";
this._el.appendChild(this._elImage);
this._bSupportDataURI = null;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _elImage = this._elImage;
var _oContext = this._oContext;
var _htOption = this._htOption;
var nCount = oQRCode.getModuleCount();
var nWidth = _htOption.width / nCount;
var nHeight = _htOption.height / nCount;
var nRoundedWidth = Math.round(nWidth);
var nRoundedHeight = Math.round(nHeight);
_elImage.style.display = "none";
this.clear();
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
var bIsDark = oQRCode.isDark(row, col);
var nLeft = col * nWidth;
var nTop = row * nHeight;
_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.lineWidth = 1;
_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
// 안티 앨리어싱 방지 처리
_oContext.strokeRect(
Math.floor(nLeft) + 0.5,
Math.floor(nTop) + 0.5,
nRoundedWidth,
nRoundedHeight
);
_oContext.strokeRect(
Math.ceil(nLeft) - 0.5,
Math.ceil(nTop) - 0.5,
nRoundedWidth,
nRoundedHeight
);
}
}
this._bIsPainted = true;
};
/**
* Make the image from Canvas if the browser supports Data URI.
*/
Drawing.prototype.makeImage = function () {
if (this._bIsPainted) {
_safeSetDataURI.call(this, _onMakeImage);
}
};
/**
* Return whether the QRCode is painted or not
*
* @return {Boolean}
*/
Drawing.prototype.isPainted = function () {
return this._bIsPainted;
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
this._bIsPainted = false;
};
/**
* @private
* @param {Number} nNumber
*/
Drawing.prototype.round = function (nNumber) {
if (!nNumber) {
return nNumber;
}
return Math.floor(nNumber * 1000) / 1000;
};
return Drawing;
})();
/**
* Get the type by string length
*
* @private
* @param {String} sText
* @param {Number} nCorrectLevel
* @return {Number} type
*/
function _getTypeNumber(sText, nCorrectLevel) {
var nType = 1;
var length = _getUTF8Length(sText);
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
var nLimit = 0;
switch (nCorrectLevel) {
case QRErrorCorrectLevel.L :
nLimit = QRCodeLimitLength[i][0];
break;
case QRErrorCorrectLevel.M :
nLimit = QRCodeLimitLength[i][1];
break;
case QRErrorCorrectLevel.Q :
nLimit = QRCodeLimitLength[i][2];
break;
case QRErrorCorrectLevel.H :
nLimit = QRCodeLimitLength[i][3];
break;
}
if (length <= nLimit) {
break;
} else {
nType++;
}
}
if (nType > QRCodeLimitLength.length) {
throw new Error("Too long data");
}
return nType;
}
function _getUTF8Length(sText) {
var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
return replacedText.length + (replacedText.length != sText ? 3 : 0);
}
/**
* @class QRCode
* @constructor
* @example
* new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
*
* @example
* var oQRCode = new QRCode("test", {
* text : "http://naver.com",
* width : 128,
* height : 128
* });
*
* oQRCode.clear(); // Clear the QRCode.
* oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
*
* @param {HTMLElement|String} el target element or 'id' attribute of element.
* @param {Object|String} vOption
* @param {String} vOption.text QRCode link data
* @param {Number} [vOption.width=256]
* @param {Number} [vOption.height=256]
* @param {String} [vOption.colorDark="#000000"]
* @param {String} [vOption.colorLight="#ffffff"]
* @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
*/
QRCode = function (el, vOption) {
this._htOption = {
width : 256,
height : 256,
typeNumber : 4,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRErrorCorrectLevel.H
};
if (typeof vOption === 'string') {
vOption = {
text : vOption
};
}
// Overwrites options
if (vOption) {
for (var i in vOption) {
this._htOption[i] = vOption[i];
}
}
if (typeof el == "string") {
el = document.getElementById(el);
}
if (this._htOption.useSVG) {
Drawing = svgDrawer;
}
this._android = _getAndroid();
this._el = el;
this._oQRCode = null;
this._oDrawing = new Drawing(this._el, this._htOption);
if (this._htOption.text) {
this.makeCode(this._htOption.text);
}
};
/**
* Make the QRCode
*
* @param {String} sText link data
*/
QRCode.prototype.makeCode = function (sText) {
this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
this._oQRCode.addData(sText);
this._oQRCode.make();
this._el.title = sText;
this._oDrawing.draw(this._oQRCode);
this.makeImage();
};
/**
* Make the Image from Canvas element
* - It occurs automatically
* - Android below 3 doesn't support Data-URI spec.
*
* @private
*/
QRCode.prototype.makeImage = function () {
if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
this._oDrawing.makeImage();
}
};
/**
* Clear the QRCode
*/
QRCode.prototype.clear = function () {
this._oDrawing.clear();
};
/**
* @name QRCode.CorrectLevel
*/
QRCode.CorrectLevel = QRErrorCorrectLevel;
})();

File diff suppressed because one or more lines are too long

View File

@ -1,29 +0,0 @@
<!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>

View File

@ -1,42 +0,0 @@
// 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', ev => {
ev.preventDefault()
clearStatus('registerStatus')
const user_name = (new FormData(regForm)).get('username')
regSubmitBtn.disabled = true
const ahandler = async () => {
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 = '/'
}, 1500)
} catch (err) {
console.error('Registration error:', err)
if (err.name === "NotAllowedError") {
showStatus('registerStatus', `Registration cancelled`, 'error')
} else {
showStatus('registerStatus', `Registration failed: ${err.message}`, 'error')
}
} finally {
regSubmitBtn.disabled = false
}
}
ahandler()
})
}
})

View File

@ -1,185 +0,0 @@
<!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>

File diff suppressed because one or more lines are too long

View File

@ -1,328 +0,0 @@
/* Passkey Authentication - Main Styles */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
width: 100%;
max-width: 400px;
text-align: center;
}
.view {
display: none;
}
.view.active {
display: block;
}
h1 {
color: #333;
margin-bottom: 30px;
font-weight: 300;
font-size: 28px;
}
h2 {
color: #555;
margin-bottom: 20px;
font-weight: 400;
font-size: 22px;
}
input[type="text"] {
width: 100%;
padding: 15px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
margin-bottom: 20px;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 15px;
margin-bottom: 15px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
border: none;
border-radius: 8px;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: transparent;
color: #667eea;
border: 2px solid #667eea;
}
.btn-secondary:hover:not(:disabled) {
background: #667eea;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
}
button:disabled {
background: #ccc !important;
cursor: not-allowed !important;
transform: none !important;
box-shadow: none !important;
}
.status {
padding: 10px;
margin: 15px 0;
border-radius: 5px;
font-size: 14px;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.credential-list {
max-height: 300px;
overflow-y: auto;
margin: 20px 0;
}
.credential-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
text-align: left;
}
.credential-item.current-session {
border: 2px solid #007bff;
background: #f8f9ff;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.2);
}
.credential-item.current-session .credential-info h4 {
color: #0056b3;
}
.credential-header {
display: grid;
grid-template-columns: 32px 1fr auto auto;
gap: 12px;
align-items: center;
margin-bottom: 10px;
}
.credential-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.auth-icon {
border-radius: 4px;
width: 32px;
height: 32px;
}
.auth-emoji {
font-size: 24px;
display: block;
text-align: center;
}
.credential-info {
min-width: 0;
}
.credential-info h4 {
margin: 0;
color: #333;
font-size: 16px;
}
.credential-dates {
text-align: right;
flex-shrink: 0;
margin-left: 20px;
display: grid;
grid-template-columns: auto auto;
gap: 5px 10px;
align-items: center;
}
.date-label {
color: #666;
font-weight: normal;
font-size: 12px;
text-align: right;
}
.date-value {
color: #333;
font-size: 12px;
text-align: left;
}
.user-info {
background: #e7f3ff;
border: 1px solid #bee5eb;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.user-info h3 {
margin: 0 0 10px 0;
color: #0c5460;
}
.user-info p {
margin: 5px 0;
color: #0c5460;
}
.toggle-link {
color: #667eea;
text-decoration: underline;
cursor: pointer;
font-size: 14px;
}
.toggle-link:hover {
color: #764ba2;
}
.hidden {
display: none;
}
.credential-actions {
display: flex;
align-items: center;
}
.btn-delete-credential {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
font-size: 16px;
color: #dc3545;
transition: background-color 0.2s;
}
.btn-delete-credential:hover:not(:disabled) {
background-color: #f8d7da;
}
.btn-delete-credential:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.token-info {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
text-align: left;
}
.token-info strong {
color: #333;
}
.token-info code {
background: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
font-family: monospace;
}
.qr-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
}
.qr-code {
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
background: white;
margin: 10px 0;
}
.link-container {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
word-break: break-all;
}
.link-container .link-text {
font-family: monospace;
font-size: 14px;
color: #495057;
margin: 0;
}

View File

@ -1,104 +0,0 @@
// 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') {
try {
await loadUserInfo()
updateUserInfo()
await 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)
}
}
}