Replace EncryptID landing page with real auth UI
Replace marketing-only landing page with a functional auth page that lets users register and sign in with passkeys. Shows profile view after login with DID, passkey list, session info, and recovery email setup. Still includes feature descriptions and r-suite app links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d0a6c3ada5
commit
fa80968b7f
|
|
@ -959,7 +959,7 @@ app.use('/dist/*', serveStatic({ root: './src/encryptid/' }));
|
||||||
app.use('/demo/*', serveStatic({ root: './src/encryptid/' }));
|
app.use('/demo/*', serveStatic({ root: './src/encryptid/' }));
|
||||||
app.use('/static/*', serveStatic({ root: './public/' }));
|
app.use('/static/*', serveStatic({ root: './public/' }));
|
||||||
|
|
||||||
// Serve index
|
// Serve index — landing page with real auth
|
||||||
app.get('/', (c) => {
|
app.get('/', (c) => {
|
||||||
return c.html(`
|
return c.html(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
@ -974,149 +974,394 @@ app.get('/', (c) => {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.container {
|
.page { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem; }
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
/* Header */
|
||||||
max-width: 600px;
|
.header { text-align: center; margin-bottom: 2.5rem; }
|
||||||
}
|
.header h1 {
|
||||||
.logo {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background: linear-gradient(90deg, #00d4ff, #7c3aed);
|
background: linear-gradient(90deg, #00d4ff, #7c3aed);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
.tagline {
|
.header .tagline { font-size: 1.1rem; color: #94a3b8; margin-top: 0.5rem; }
|
||||||
font-size: 1.25rem;
|
|
||||||
color: #94a3b8;
|
/* Auth card */
|
||||||
margin-bottom: 2rem;
|
.auth-card {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
.features {
|
.auth-card h2 { font-size: 1.25rem; margin-bottom: 0.25rem; }
|
||||||
display: grid;
|
.auth-card .sub { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1.5rem; }
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 1rem;
|
/* Tabs */
|
||||||
margin-bottom: 2rem;
|
.tabs { display: flex; border-radius: 0.5rem; overflow: hidden; border: 1px solid rgba(255,255,255,0.15); margin-bottom: 1.5rem; }
|
||||||
|
.tab { flex: 1; padding: 0.6rem; text-align: center; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: background 0.2s; background: transparent; color: #94a3b8; border: none; }
|
||||||
|
.tab.active { background: linear-gradient(90deg, #00d4ff, #7c3aed); color: #fff; }
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.form-group { margin-bottom: 1rem; }
|
||||||
|
.form-group label { display: block; font-size: 0.8rem; color: #94a3b8; margin-bottom: 0.4rem; font-weight: 500; }
|
||||||
|
.form-group input {
|
||||||
|
width: 100%; padding: 0.7rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
background: rgba(255,255,255,0.05); color: #fff; font-size: 0.95rem; outline: none; transition: border 0.2s;
|
||||||
}
|
}
|
||||||
.feature {
|
.form-group input:focus { border-color: #7c3aed; }
|
||||||
background: rgba(255,255,255,0.05);
|
.form-group input::placeholder { color: #475569; }
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
.btn-primary {
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
width: 100%; padding: 0.75rem; border-radius: 0.5rem; border: none;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #7c3aed); color: #fff;
|
||||||
|
font-size: 1rem; font-weight: 600; cursor: pointer; transition: transform 0.15s, opacity 0.15s;
|
||||||
}
|
}
|
||||||
.feature-icon {
|
.btn-primary:hover { transform: translateY(-1px); }
|
||||||
font-size: 1.5rem;
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
.error { background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); border-radius: 0.5rem; padding: 0.6rem 1rem; font-size: 0.85rem; color: #fca5a5; margin-bottom: 1rem; display: none; }
|
||||||
|
.success { background: rgba(34,197,94,0.15); border: 1px solid rgba(34,197,94,0.3); border-radius: 0.5rem; padding: 0.6rem 1rem; font-size: 0.85rem; color: #86efac; margin-bottom: 1rem; display: none; }
|
||||||
|
|
||||||
|
/* Profile (shown when logged in) */
|
||||||
|
.profile { display: none; }
|
||||||
|
.profile-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
|
||||||
|
.profile-avatar { width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #00d4ff, #7c3aed); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; font-weight: 700; flex-shrink: 0; }
|
||||||
|
.profile-info h3 { font-size: 1.1rem; }
|
||||||
|
.profile-info .did { font-family: monospace; font-size: 0.75rem; color: #94a3b8; word-break: break-all; }
|
||||||
|
.profile-row { display: flex; justify-content: space-between; padding: 0.6rem 0; border-bottom: 1px solid rgba(255,255,255,0.08); font-size: 0.875rem; }
|
||||||
|
.profile-row .label { color: #94a3b8; }
|
||||||
|
.profile-actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; flex-wrap: wrap; }
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 0.6rem 1.25rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
background: transparent; color: #fff; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: background 0.2s;
|
||||||
}
|
}
|
||||||
.feature-title {
|
.btn-secondary:hover { background: rgba(255,255,255,0.08); }
|
||||||
font-weight: 600;
|
.btn-danger { border-color: rgba(239,68,68,0.4); color: #fca5a5; }
|
||||||
margin-bottom: 0.25rem;
|
.btn-danger:hover { background: rgba(239,68,68,0.15); }
|
||||||
}
|
|
||||||
.feature-desc {
|
/* Passkeys list */
|
||||||
font-size: 0.875rem;
|
.passkeys { margin-top: 1.5rem; }
|
||||||
color: #94a3b8;
|
.passkeys h4 { font-size: 0.9rem; margin-bottom: 0.75rem; color: #94a3b8; }
|
||||||
}
|
.passkey-item { display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0.75rem; background: rgba(255,255,255,0.04); border-radius: 0.5rem; margin-bottom: 0.5rem; font-size: 0.8rem; }
|
||||||
.btn {
|
.passkey-id { font-family: monospace; color: #94a3b8; }
|
||||||
display: inline-block;
|
.passkey-date { color: #64748b; }
|
||||||
padding: 0.75rem 2rem;
|
|
||||||
background: linear-gradient(90deg, #00d4ff, #7c3aed);
|
/* Recovery email */
|
||||||
color: #fff;
|
.recovery-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.08); }
|
||||||
text-decoration: none;
|
.recovery-section h4 { font-size: 0.9rem; margin-bottom: 0.75rem; color: #94a3b8; }
|
||||||
border-radius: 0.5rem;
|
.recovery-row { display: flex; gap: 0.5rem; }
|
||||||
font-weight: 600;
|
.recovery-row input { flex: 1; }
|
||||||
margin: 0.5rem;
|
.recovery-row button { white-space: nowrap; }
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
/* Features */
|
||||||
.btn:hover {
|
.features { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
||||||
transform: translateY(-2px);
|
.feature { background: rgba(255,255,255,0.04); padding: 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.08); text-align: center; }
|
||||||
}
|
.feature-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.25rem; }
|
||||||
.btn-outline {
|
.feature-desc { font-size: 0.8rem; color: #94a3b8; }
|
||||||
background: transparent;
|
|
||||||
border: 2px solid #7c3aed;
|
/* Apps bar */
|
||||||
}
|
.apps { text-align: center; padding-top: 2rem; border-top: 1px solid rgba(255,255,255,0.08); }
|
||||||
.apps {
|
.apps-title { font-size: 0.8rem; color: #64748b; margin-bottom: 0.75rem; }
|
||||||
margin-top: 2rem;
|
.app-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem; }
|
||||||
padding-top: 2rem;
|
.app-links a { color: #64748b; text-decoration: none; font-size: 0.85rem; transition: color 0.2s; }
|
||||||
border-top: 1px solid rgba(255,255,255,0.1);
|
.app-links a:hover { color: #00d4ff; }
|
||||||
}
|
|
||||||
.apps-title {
|
.link-row { text-align: center; margin-top: 1rem; font-size: 0.8rem; }
|
||||||
font-size: 0.875rem;
|
.link-row a { color: #7c3aed; text-decoration: none; }
|
||||||
color: #64748b;
|
.link-row a:hover { text-decoration: underline; }
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
@media (max-width: 480px) {
|
||||||
.app-icons {
|
.features { grid-template-columns: 1fr; }
|
||||||
display: flex;
|
.header h1 { font-size: 2rem; }
|
||||||
justify-content: center;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
.app-icon {
|
|
||||||
color: #64748b;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.app-icon:hover {
|
|
||||||
color: #00d4ff;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="page">
|
||||||
<div class="logo">🔐</div>
|
<div class="header">
|
||||||
<h1>EncryptID</h1>
|
<h1>EncryptID</h1>
|
||||||
<p class="tagline">Unified Identity for the r-Ecosystem</p>
|
<p class="tagline">Unified Identity for the r-Ecosystem</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth card: login/register when signed out, profile when signed in -->
|
||||||
|
<div class="auth-card">
|
||||||
|
<!-- Login/Register form -->
|
||||||
|
<div id="auth-form">
|
||||||
|
<h2>Get started</h2>
|
||||||
|
<p class="sub">Sign in with your passkey or create a new account. No passwords, no tracking.</p>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active" id="tab-signin" onclick="switchTab('signin')">Sign In</button>
|
||||||
|
<button class="tab" id="tab-register" onclick="switchTab('register')">Register</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error-msg" class="error"></div>
|
||||||
|
<div id="success-msg" class="success"></div>
|
||||||
|
|
||||||
|
<!-- Register fields (hidden in signin mode) -->
|
||||||
|
<div id="register-fields" style="display:none">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input id="username" type="text" placeholder="Choose a username" autocomplete="username" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-primary" id="auth-btn" onclick="handleAuth()">Sign In with Passkey</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile (shown when logged in) -->
|
||||||
|
<div id="profile" class="profile">
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="profile-avatar" id="profile-avatar">?</div>
|
||||||
|
<div class="profile-info">
|
||||||
|
<h3 id="profile-username">Loading...</h3>
|
||||||
|
<div class="did" id="profile-did"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-row"><span class="label">Session</span><span id="profile-session">Active</span></div>
|
||||||
|
<div class="profile-row"><span class="label">Token expires</span><span id="profile-expires">--</span></div>
|
||||||
|
|
||||||
|
<div class="passkeys">
|
||||||
|
<h4>Your Passkeys</h4>
|
||||||
|
<div id="passkey-list"><div class="passkey-item"><span class="passkey-id">Loading...</span></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recovery-section">
|
||||||
|
<h4>Recovery Email</h4>
|
||||||
|
<p style="font-size:0.8rem;color:#64748b;margin-bottom:0.75rem">Set an email to recover your account if you lose your passkey.</p>
|
||||||
|
<div class="recovery-row">
|
||||||
|
<input id="recovery-email" type="email" placeholder="you@example.com" style="padding:0.5rem 0.75rem;border-radius:0.5rem;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.05);color:#fff;font-size:0.85rem;outline:none" />
|
||||||
|
<button class="btn-secondary" onclick="setRecoveryEmail()">Save</button>
|
||||||
|
</div>
|
||||||
|
<div id="recovery-msg" style="font-size:0.8rem;margin-top:0.5rem;color:#86efac;display:none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-actions">
|
||||||
|
<a href="/demo.html" class="btn-secondary">SDK Demo</a>
|
||||||
|
<button class="btn-secondary btn-danger" onclick="handleLogout()">Sign Out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="features">
|
<div class="features">
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">🔑</div>
|
|
||||||
<div class="feature-title">Passkey Auth</div>
|
<div class="feature-title">Passkey Auth</div>
|
||||||
<div class="feature-desc">Hardware-backed, phishing-resistant</div>
|
<div class="feature-desc">Hardware-backed, phishing-resistant login. No passwords ever.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">🛡️</div>
|
|
||||||
<div class="feature-title">Social Recovery</div>
|
<div class="feature-title">Social Recovery</div>
|
||||||
<div class="feature-desc">No seed phrases needed</div>
|
<div class="feature-desc">Recover your account via email. No seed phrases needed.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">🔐</div>
|
|
||||||
<div class="feature-title">E2E Encryption</div>
|
<div class="feature-title">E2E Encryption</div>
|
||||||
<div class="feature-desc">Keys never leave your device</div>
|
<div class="feature-desc">Derive keys from your passkey. Keys never leave your device.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">💰</div>
|
<div class="feature-title">Cross-App Identity</div>
|
||||||
<div class="feature-title">Web3 Ready</div>
|
<div class="feature-desc">One passkey works across every app in the r-suite.</div>
|
||||||
<div class="feature-desc">Account abstraction wallets</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/demo.html" class="btn">Try Demo</a>
|
|
||||||
<a href="https://github.com/jeffemmett/rspace-online" class="btn btn-outline">GitHub</a>
|
|
||||||
|
|
||||||
<div class="apps">
|
<div class="apps">
|
||||||
<div class="apps-title">Works with the r-Ecosystem</div>
|
<div class="apps-title">One identity across the r-Ecosystem</div>
|
||||||
<div class="app-icons">
|
<div class="app-links">
|
||||||
<a href="https://rspace.online" class="app-icon">rSpace</a>
|
<a href="https://rspace.online">rSpace</a>
|
||||||
<a href="https://rwallet.online" class="app-icon">rWallet</a>
|
<a href="https://rnotes.online">rNotes</a>
|
||||||
<a href="https://rvote.online" class="app-icon">rVote</a>
|
<a href="https://rfiles.online">rFiles</a>
|
||||||
<a href="https://rmaps.online" class="app-icon">rMaps</a>
|
<a href="https://rcart.online">rCart</a>
|
||||||
<a href="https://rfiles.online" class="app-icon">rFiles</a>
|
<a href="https://rfunds.online">rFunds</a>
|
||||||
<a href="https://rnotes.online" class="app-icon">rNotes</a>
|
<a href="https://rauctions.online">rAuctions</a>
|
||||||
<a href="https://rtrips.online" class="app-icon">rTrips</a>
|
<a href="https://rpubs.online">rPubs</a>
|
||||||
<a href="https://rfunds.online" class="app-icon">rFunds</a>
|
<a href="https://rvote.online">rVote</a>
|
||||||
<a href="https://rnetwork.online" class="app-icon">rNetwork</a>
|
<a href="https://rmaps.online">rMaps</a>
|
||||||
<a href="https://rcart.online" class="app-icon">rCart</a>
|
<a href="https://rstack.online">rStack</a>
|
||||||
<a href="https://rtube.online" class="app-icon">rTube</a>
|
|
||||||
<a href="https://rstack.online" class="app-icon">rStack</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import {
|
||||||
|
registerPasskey,
|
||||||
|
authenticatePasskey,
|
||||||
|
getKeyManager,
|
||||||
|
getSessionManager,
|
||||||
|
detectCapabilities,
|
||||||
|
} from '/dist/index.js';
|
||||||
|
|
||||||
|
const TOKEN_KEY = 'encryptid_token';
|
||||||
|
let currentMode = 'signin';
|
||||||
|
|
||||||
|
// Expose to inline onclick handlers
|
||||||
|
window.switchTab = (mode) => {
|
||||||
|
currentMode = mode;
|
||||||
|
document.getElementById('tab-signin').classList.toggle('active', mode === 'signin');
|
||||||
|
document.getElementById('tab-register').classList.toggle('active', mode === 'register');
|
||||||
|
document.getElementById('register-fields').style.display = mode === 'register' ? 'block' : 'none';
|
||||||
|
document.getElementById('auth-btn').textContent = mode === 'signin' ? 'Sign In with Passkey' : 'Register with Passkey';
|
||||||
|
hideMessages();
|
||||||
|
};
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
const el = document.getElementById('error-msg');
|
||||||
|
el.textContent = msg; el.style.display = 'block';
|
||||||
|
document.getElementById('success-msg').style.display = 'none';
|
||||||
|
}
|
||||||
|
function showSuccess(msg) {
|
||||||
|
const el = document.getElementById('success-msg');
|
||||||
|
el.textContent = msg; el.style.display = 'block';
|
||||||
|
document.getElementById('error-msg').style.display = 'none';
|
||||||
|
}
|
||||||
|
function hideMessages() {
|
||||||
|
document.getElementById('error-msg').style.display = 'none';
|
||||||
|
document.getElementById('success-msg').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleAuth = async () => {
|
||||||
|
const btn = document.getElementById('auth-btn');
|
||||||
|
btn.disabled = true;
|
||||||
|
hideMessages();
|
||||||
|
try {
|
||||||
|
if (currentMode === 'register') {
|
||||||
|
const username = document.getElementById('username').value.trim();
|
||||||
|
if (!username) { showError('Username is required'); btn.disabled = false; return; }
|
||||||
|
|
||||||
|
btn.textContent = 'Creating passkey...';
|
||||||
|
const credential = await registerPasskey(username, username);
|
||||||
|
|
||||||
|
// Complete registration with server
|
||||||
|
const res = await fetch('/api/register/complete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
challenge: credential.challenge,
|
||||||
|
credential: credential.credential,
|
||||||
|
userId: credential.userId,
|
||||||
|
username,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Registration failed');
|
||||||
|
|
||||||
|
localStorage.setItem(TOKEN_KEY, data.token);
|
||||||
|
showProfile(data.token, username, data.did);
|
||||||
|
} else {
|
||||||
|
btn.textContent = 'Waiting for passkey...';
|
||||||
|
const result = await authenticatePasskey();
|
||||||
|
|
||||||
|
// Complete auth with server
|
||||||
|
const res = await fetch('/api/auth/complete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
challenge: result.challenge,
|
||||||
|
credential: result.credential,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Authentication failed');
|
||||||
|
|
||||||
|
localStorage.setItem(TOKEN_KEY, data.token);
|
||||||
|
showProfile(data.token, data.username, data.did);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message || 'Authentication failed');
|
||||||
|
btn.textContent = currentMode === 'signin' ? 'Sign In with Passkey' : 'Register with Passkey';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function showProfile(token, username, did) {
|
||||||
|
document.getElementById('auth-form').style.display = 'none';
|
||||||
|
document.getElementById('profile').style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('profile-username').textContent = username || 'Anonymous';
|
||||||
|
document.getElementById('profile-avatar').textContent = (username || '?')[0].toUpperCase();
|
||||||
|
document.getElementById('profile-did').textContent = did || '';
|
||||||
|
|
||||||
|
// Parse token expiry
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
|
const exp = new Date(payload.exp * 1000);
|
||||||
|
document.getElementById('profile-expires').textContent = exp.toLocaleString();
|
||||||
|
} catch { document.getElementById('profile-expires').textContent = '--'; }
|
||||||
|
|
||||||
|
// Fetch passkeys
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/user/credentials', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const list = document.getElementById('passkey-list');
|
||||||
|
if (data.credentials && data.credentials.length > 0) {
|
||||||
|
list.innerHTML = data.credentials.map(c => {
|
||||||
|
const created = c.createdAt ? new Date(c.createdAt).toLocaleDateString() : '';
|
||||||
|
return '<div class="passkey-item"><span class="passkey-id">' +
|
||||||
|
c.credentialId.slice(0, 24) + '...</span><span class="passkey-date">' + created + '</span></div>';
|
||||||
|
}).join('');
|
||||||
|
} else {
|
||||||
|
list.innerHTML = '<div class="passkey-item"><span class="passkey-id">No passkeys found</span></div>';
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleLogout = () => {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
document.getElementById('auth-form').style.display = 'block';
|
||||||
|
document.getElementById('profile').style.display = 'none';
|
||||||
|
const btn = document.getElementById('auth-btn');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Sign In with Passkey';
|
||||||
|
currentMode = 'signin';
|
||||||
|
switchTab('signin');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setRecoveryEmail = async () => {
|
||||||
|
const email = document.getElementById('recovery-email').value.trim();
|
||||||
|
if (!email) return;
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
|
if (!token) return;
|
||||||
|
const msgEl = document.getElementById('recovery-msg');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/recovery/email/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Failed');
|
||||||
|
msgEl.textContent = 'Recovery email saved.';
|
||||||
|
msgEl.style.color = '#86efac';
|
||||||
|
msgEl.style.display = 'block';
|
||||||
|
} catch (err) {
|
||||||
|
msgEl.textContent = err.message;
|
||||||
|
msgEl.style.color = '#fca5a5';
|
||||||
|
msgEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// On page load: check for existing token
|
||||||
|
(async () => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/session/verify', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.valid) {
|
||||||
|
showProfile(token, data.username, data.did);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue