alotta good shit
This commit is contained in:
+272
-165
@@ -66,7 +66,7 @@
|
||||
@endif
|
||||
@endif
|
||||
@if(enable_anonymous_access && session.is_anon)
|
||||
<a href="#" id="nav-user-anon-identity-btn" onclick="event.preventDefault(); if(window.f0ckAnonSSH) { window.f0ckAnonSSH.openModal(); } else { const m = document.getElementById('anon-ssh-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-key"></i> Key Management</a>
|
||||
<a href="#" id="nav-user-anon-identity-btn" onclick="event.preventDefault(); if(window.f0ckAnonPasskey) { window.f0ckAnonPasskey.openModal(); } else { const m = document.getElementById('anon-passkey-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-fingerprint"></i> Passkey Identity</a>
|
||||
@endif
|
||||
<a href="/user/{{ (session.is_anon && session.login ? session.login : session.user).toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
|
||||
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
|
||||
@@ -84,20 +84,30 @@
|
||||
<div id="notif-dropdown" class="notif-dropdown">
|
||||
<div class="notif-header">
|
||||
<div class="notif-tabs">
|
||||
@if(enable_comments)
|
||||
<button class="notif-tab active" data-tab="user">{{ t('nav.notif_tab_user') }} <span class="notif-tab-badge" id="notif-tab-badge-user" style="display:none">0</span></button>
|
||||
<button class="notif-tab" data-tab="system">{{ t('nav.notif_tab_system') }} <span class="notif-tab-badge" id="notif-tab-badge-system" style="display:none">0</span></button>
|
||||
@else
|
||||
<button class="notif-tab active" data-tab="system">{{ t('nav.notif_tab_system') }} <span class="notif-tab-badge" id="notif-tab-badge-system" style="display:none">0</span></button>
|
||||
@endif
|
||||
</div>
|
||||
<button id="mark-all-read" title="{{ t('nav.mark_all_read') }}"><i class="fa-solid fa-check-double"></i></button>
|
||||
</div>
|
||||
@if(enable_comments)
|
||||
<div class="notif-list" data-active-tab="user">
|
||||
@else
|
||||
<div class="notif-list" data-active-tab="system">
|
||||
@endif
|
||||
<div class="notif-empty">{{ t('nav.no_notifications') }}</div>
|
||||
</div>
|
||||
<div class="notif-footer">
|
||||
<a href="/notifications" class="view-all-notifs">{{ t('nav.view_all_notifications') }}</a>
|
||||
</div>
|
||||
@if(enable_comments)
|
||||
<div class="submanage">
|
||||
<a href="/subscriptions">{{ t('nav.manage_subscriptions') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -286,7 +296,7 @@
|
||||
@endif
|
||||
@if(enable_anonymous_access)
|
||||
|
||||
<a href="#" id="nav-anon-identity-btn" @if(!session || !session.is_anon) style="display:none;" @endif onclick="event.preventDefault(); if(window.f0ckAnonSSH) { window.f0ckAnonSSH.openModal(); } else { const m = document.getElementById('anon-ssh-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-key"></i> Key Management</a>
|
||||
<a href="#" id="nav-anon-identity-btn" @if(!session || !session.is_anon) style="display:none;" @endif onclick="event.preventDefault(); if(window.f0ckAnonPasskey) { window.f0ckAnonPasskey.openModal(); } else { const m = document.getElementById('anon-passkey-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-fingerprint"></i> Passkey Identity</a>
|
||||
<a href="/settings" id="nav-anon-settings-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-gear"></i> Settings</a>
|
||||
@endif
|
||||
<a href="#" id="nav-login-btn">{{ t('nav.login') }}</a>
|
||||
@@ -318,40 +328,141 @@
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
// Passkey login for registered users — fires only on explicit button click
|
||||
async function loginWithPasskey() {
|
||||
const btn = document.getElementById('modal-login-passkey-btn');
|
||||
const errEl = document.getElementById('modal-passkey-error');
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
if (!window.PublicKeyCredential) {
|
||||
if (errEl) { errEl.style.display = ''; errEl.textContent = 'Passkeys are not supported in this browser.'; }
|
||||
return;
|
||||
}
|
||||
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Waiting for passkey...'; }
|
||||
|
||||
function b64urlToArr(s) { return Uint8Array.from(atob(s.split('-').join('+').split('_').join('/')), c=>c.charCodeAt(0)); }
|
||||
function arrToB64url(buf) { return btoa(String.fromCharCode(...new Uint8Array(buf))).split('+').join('-').split('/').join('_').replace(/=+$/,''); }
|
||||
|
||||
try {
|
||||
const beginRes = await fetch('/api/v2/settings/passkeys/login/begin', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' });
|
||||
const beginData = await beginRes.json();
|
||||
if (!beginData.success) throw new Error(beginData.msg || 'Server error');
|
||||
|
||||
const opts = beginData.options;
|
||||
const cred = await navigator.credentials.get({ publicKey: {
|
||||
challenge: b64urlToArr(opts.challenge),
|
||||
rpId: opts.rpId,
|
||||
allowCredentials: (opts.allowCredentials || []).map(c => ({ type: c.type, id: b64urlToArr(c.id) })),
|
||||
userVerification: opts.userVerification || 'preferred',
|
||||
timeout: opts.timeout || 60000
|
||||
}});
|
||||
|
||||
const finishRes = await fetch('/api/v2/settings/passkeys/login/finish', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
challenge: opts.challenge,
|
||||
credentialId: arrToB64url(cred.rawId),
|
||||
clientDataJSON: arrToB64url(cred.response.clientDataJSON),
|
||||
authenticatorData: arrToB64url(cred.response.authenticatorData),
|
||||
signature: arrToB64url(cred.response.signature)
|
||||
})
|
||||
});
|
||||
const finishData = await finishRes.json();
|
||||
if (!finishData.success) throw new Error(finishData.msg || 'Authentication failed');
|
||||
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
if (e && e.name !== 'NotAllowedError' && errEl) {
|
||||
errEl.style.display = '';
|
||||
errEl.textContent = e.message || 'Passkey login failed.';
|
||||
}
|
||||
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Login with passkey'; }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="login-modal" style="display: none;">
|
||||
<div class="login-modal-content">
|
||||
<button id="login-modal-close">×</button>
|
||||
|
||||
<!-- Login View -->
|
||||
<div id="modal-login-view">
|
||||
<form class="login-form" method="post" action="/login" novalidate>
|
||||
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.login_title') }}</h2>
|
||||
<input type="text" name="username" placeholder="{{ t('auth.username_or_email') }}" autocomplete="off" required />
|
||||
<input type="password" name="password" placeholder="{{ t('auth.password_placeholder_min') }}" autocomplete="off" required />
|
||||
<p style="text-align: left; font-size: 0.9em; margin: 0;"><input type="checkbox" id="kmsi-modal" name="kmsi" />
|
||||
<label for="kmsi-modal">{{ t('auth.stay_signed_in') }}</label>
|
||||
<h2 style="text-align: center; margin-bottom: 18px;">{{ t('auth.login_title') }}</h2>
|
||||
|
||||
<!-- Tab bar -->
|
||||
<div id="login-tabs" style="display: flex; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 20px;">
|
||||
<button type="button" id="login-tab-creds" onclick="switchLoginTab('creds')"
|
||||
style="flex: 1; background: none; border: none; border-bottom: 2px solid var(--accent, #0096ff); color: var(--text-color, #fff); padding: 8px 0; font-size: 0.9em; cursor: pointer; font-weight: 600; transition: color 0.15s;">
|
||||
<i class="fa-solid fa-key" style="margin-right:5px;"></i>Credentials
|
||||
</button>
|
||||
<button type="button" id="login-tab-passkey" onclick="switchLoginTab('passkey')"
|
||||
style="flex: 1; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted, #888); padding: 8px 0; font-size: 0.9em; cursor: pointer; font-weight: 600; transition: color 0.15s;">
|
||||
<i class="fa-solid fa-fingerprint" style="margin-right:5px;"></i>Passkey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Credentials -->
|
||||
<div id="login-panel-creds">
|
||||
<form class="login-form" method="post" action="/login" novalidate style="padding: 0; background: none; box-shadow: none; border: none;">
|
||||
<input type="text" name="username" placeholder="{{ t('auth.username_or_email') }}" autocomplete="off" required />
|
||||
<input type="password" name="password" placeholder="{{ t('auth.password_placeholder_min') }}" autocomplete="off" required />
|
||||
<p style="text-align: left; font-size: 0.9em; margin: 0;"><input type="checkbox" id="kmsi-modal" name="kmsi" />
|
||||
<label for="kmsi-modal">{{ t('auth.stay_signed_in') }}</label>
|
||||
</p>
|
||||
<button type="submit">{{ t('auth.login_title') }}</button>
|
||||
@if(smtp_enabled)
|
||||
<div style="text-align: center; margin-top: 10px;">
|
||||
<a href="#" id="modal-forgot-btn" style="font-size: 0.85em; color: var(--accent); text-decoration: underline;">{{ t('auth.forgot_password') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@if(registration_open || private_society)
|
||||
<p style="text-align: center; font-size: 0.9em; margin-top: 15px; color: #888;">
|
||||
{{ t('auth.no_account') }} <a href="#" id="login-to-register" style="color: var(--accent); text-decoration: underline;">{{ t('auth.register_now') }}</a>
|
||||
</p>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Passkey -->
|
||||
<div id="login-panel-passkey" style="display: none; text-align: center; padding: 8px 0 4px;">
|
||||
<div style="font-size: 2.6em; margin-bottom: 14px;">🔑</div>
|
||||
<p style="font-size: 0.88em; color: var(--text-muted, #aaa); margin: 0 0 20px; line-height: 1.55;">
|
||||
Use a saved passkey from Bitwarden, iCloud Keychain, or your OS to sign in without a password.
|
||||
</p>
|
||||
<button type="submit">{{ t('auth.login_title') }}</button>
|
||||
@if(smtp_enabled)
|
||||
<div style="text-align: center; margin-top: 10px;">
|
||||
<a href="#" id="modal-forgot-btn" style="font-size: 0.85em; color: var(--accent); text-decoration: underline;">{{ t('auth.forgot_password') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@if(registration_open || private_society)
|
||||
<p style="text-align: center; font-size: 0.9em; margin-top: 15px; color: #888;">
|
||||
{{ t('auth.no_account') }} <a href="#" id="login-to-register" style="color: var(--accent); text-decoration: underline;">{{ t('auth.register_now') }}</a>
|
||||
</p>
|
||||
@endif
|
||||
@if(enable_anonymous_access)
|
||||
<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; text-align: center;">
|
||||
<button type="button" id="modal-login-as-anon-btn" class="btn btn-sm" style="background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.2); color: #ddd; border-radius: 4px; padding: 7px 14px; font-size: 0.88em; cursor: pointer; width: 100%; transition: background 0.2s;">
|
||||
<i class="fa-solid fa-user-secret"></i> Login as anonymous
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
<button type="button" id="modal-login-passkey-btn"
|
||||
style="width: 100%; padding: 11px; background: var(--accent, #0096ff); color: #fff; border: none; border-radius: 5px; font-size: 0.95em; font-weight: 600; cursor: pointer; margin-bottom: 8px;"
|
||||
onclick="loginWithPasskey()">
|
||||
<i class="fa-solid fa-fingerprint"></i> Use my passkey
|
||||
</button>
|
||||
<div id="modal-passkey-error" style="display:none; color:#e06c75; font-size:0.82em; margin-top:6px;"></div>
|
||||
</div>
|
||||
|
||||
@if(enable_anonymous_access)
|
||||
<div style="margin-top: 16px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 12px; text-align: center;">
|
||||
<button type="button" id="modal-login-as-anon-btn"
|
||||
style="background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); color: #999; border-radius: 4px; padding: 7px 14px; font-size: 0.85em; cursor: pointer; width: 100%; transition: background 0.2s;"
|
||||
onclick="event.preventDefault(); if(window.f0ckAnonPasskey) window.f0ckAnonPasskey.openSetupModal(); else { const m=document.getElementById('anon-setup-modal'); if(m) m.style.display='flex'; }">
|
||||
<i class="fa-solid fa-user-secret"></i> Login as anonymous
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchLoginTab(tab) {
|
||||
var isCreds = tab === 'creds';
|
||||
document.getElementById('login-panel-creds').style.display = isCreds ? '' : 'none';
|
||||
document.getElementById('login-panel-passkey').style.display = isCreds ? 'none' : '';
|
||||
var tCreds = document.getElementById('login-tab-creds');
|
||||
var tPk = document.getElementById('login-tab-passkey');
|
||||
if (tCreds) { tCreds.style.borderBottomColor = isCreds ? 'var(--accent, #0096ff)' : 'transparent'; tCreds.style.color = isCreds ? 'var(--text-color, #fff)' : 'var(--text-muted, #888)'; }
|
||||
if (tPk) { tPk.style.borderBottomColor = isCreds ? 'transparent' : 'var(--accent, #0096ff)'; tPk.style.color = isCreds ? 'var(--text-muted, #888)' : 'var(--text-color, #fff)'; }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@if(smtp_enabled)
|
||||
<!-- Forgot Password View -->
|
||||
<div id="modal-forgot-view" style="display: none;">
|
||||
@@ -387,169 +498,165 @@
|
||||
</div>
|
||||
|
||||
@if(enable_anonymous_access)
|
||||
<!-- Anonymous OpenSSH Ed25519 Identity Modal -->
|
||||
<div id="anon-ssh-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); z-index: 99999; align-items: center; justify-content: center;">
|
||||
<div class="login-modal-content" style="max-width: 540px; width: 92vw; max-height: 90vh; overflow-y: auto; text-align: left; padding: 25px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: var(--bg-primary, #111); box-shadow: 0 10px 40px rgba(0,0,0,0.8);">
|
||||
<button id="anon-ssh-modal-close" style="position: absolute; top: 15px; right: 15px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.4em; cursor: pointer; line-height: 1;">×</button>
|
||||
|
||||
<!-- Anonymous Passkey Identity Modal -->
|
||||
<div id="anon-passkey-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); z-index: 99999; align-items: center; justify-content: center;">
|
||||
<div class="login-modal-content" style="max-width: 500px; width: 92vw; max-height: 90vh; overflow-y: auto; text-align: left; padding: 25px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: var(--bg-primary, #111); box-shadow: 0 10px 40px rgba(0,0,0,0.8); position: relative;">
|
||||
<button id="anon-passkey-modal-close" style="position: absolute; top: 15px; right: 15px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.4em; cursor: pointer; line-height: 1;">×</button>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||||
<i class="fa-solid fa-key" style="color: var(--accent, #0096ff); font-size: 1.3em;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.25em; color: var(--text-color, #fff);">Anonymous SSH Identity</h3>
|
||||
<i class="fa-solid fa-fingerprint" style="color: var(--accent, #0096ff); font-size: 1.3em;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.25em; color: var(--text-color, #fff);">Anonymous Passkey Identity</h3>
|
||||
</div>
|
||||
<p style="margin: 0 0 15px 0; font-size: 0.85em; color: var(--text-muted, #aaa); line-height: 1.4;">
|
||||
Your browser holds an <strong>OpenSSH Ed25519</strong> private key. Your comments and favorites belong to this key without needing a password.
|
||||
<p style="margin: 0 0 18px 0; font-size: 0.85em; color: var(--text-muted, #aaa); line-height: 1.4;">
|
||||
Your anonymous identity is protected by a <strong>passkey</strong> stored in your OS or password manager (e.g. Bitwarden).
|
||||
No private key ever touches this browser's storage.
|
||||
</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px;">
|
||||
<button type="button" id="anon-tab-btn-identity" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 5px 12px;">My Identity</button>
|
||||
<button type="button" id="anon-tab-btn-import" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 5px 12px;">Import / Supply Key</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Current Identity -->
|
||||
<div id="anon-tab-identity">
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display: block; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 4px;">Fingerprint</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: rgba(0,0,0,0.4); padding: 8px 12px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.08);">
|
||||
<code id="anon-ssh-fp-display" style="font-family: monospace; font-size: 0.88em; color: var(--accent, #00d2ff); word-break: break-all; flex: 1;">Generating...</code>
|
||||
<button type="button" id="anon-copy-fp-btn" title="Copy Fingerprint" class="btn btn-sm" style="background: transparent; border: none; color: #aaa; cursor: pointer; padding: 4px;"><i class="fa-solid fa-copy"></i></button>
|
||||
</div>
|
||||
<!-- Identity info -->
|
||||
<div style="margin-bottom: 14px; padding: 12px; background: rgba(0,0,0,0.35); border-radius: 6px; border: 1px solid rgba(255,255,255,0.07);">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="display: block; font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 3px;">Short Fingerprint</label>
|
||||
<code id="anon-pk-fp-display" style="font-family: monospace; font-size: 1em; color: var(--accent, #00d2ff);">—</code>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 4px;">OpenSSH Public Key (<code>id_ed25519.pub</code>)</label>
|
||||
<textarea id="anon-ssh-pub-display" readonly rows="2" style="width: 100%; box-sizing: border-box; font-family: monospace; font-size: 0.82em; background: rgba(0,0,0,0.4); color: #ddd; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 8px; resize: none;"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<button type="button" id="anon-copy-pub-btn" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-copy"></i> Copy Public Key</button>
|
||||
<button type="button" id="anon-dl-priv-btn" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-download"></i> Download id_ed25519</button>
|
||||
<button type="button" id="anon-dl-pub-btn" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-download"></i> Download id_ed25519.pub</button>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 3px;">Credential ID (short)</label>
|
||||
<code id="anon-pk-cred-display" style="font-family: monospace; font-size: 0.78em; color: var(--text-muted, #aaa); word-break: break-all;">—</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Import Key -->
|
||||
<div id="anon-tab-import" style="display: none;">
|
||||
<p style="font-size: 0.85em; color: var(--text-muted, #aaa); margin-top: 0; margin-bottom: 10px;">
|
||||
Paste your existing <code>id_ed25519</code> OpenSSH private key or raw 32-byte seed to restore your anonymous identity on this browser.
|
||||
</p>
|
||||
<textarea id="anon-import-key-input" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----" style="width: 100%; box-sizing: border-box; font-family: monospace; font-size: 0.82em; background: rgba(0,0,0,0.4); color: #ddd; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 8px; resize: vertical; margin-bottom: 10px;"></textarea>
|
||||
|
||||
<input type="file" id="anon-import-file-elem" style="display: none;" />
|
||||
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<button type="button" id="anon-upload-key-btn" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-upload"></i> Upload File</button>
|
||||
<button type="button" id="anon-submit-import-btn" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 6px 16px;"><i class="fa-solid fa-check"></i> Activate Key</button>
|
||||
</div>
|
||||
<div id="anon-import-status" style="margin-top: 10px; font-size: 0.85em; display: none;"></div>
|
||||
<!-- Actions -->
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<button type="button" id="anon-pk-add-btn" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 6px 14px;">
|
||||
<i class="fa-solid fa-plus"></i> Add another passkey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style="margin: 14px 0 0; font-size: 0.8em; color: var(--text-muted, #888); line-height: 1.4;">
|
||||
To use this identity on another device, simply sign in with your passkey manager (Bitwarden, iCloud Keychain, etc.) — it syncs automatically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// Tab switching
|
||||
const tabBtnId = document.getElementById('anon-tab-btn-identity');
|
||||
const tabBtnImp = document.getElementById('anon-tab-btn-import');
|
||||
const tabId = document.getElementById('anon-tab-identity');
|
||||
const tabImp = document.getElementById('anon-tab-import');
|
||||
if(tabBtnId && tabBtnImp && tabId && tabImp){
|
||||
tabBtnId.addEventListener('click', function(){
|
||||
tabId.style.display = 'block';
|
||||
tabImp.style.display = 'none';
|
||||
tabBtnId.classList.remove('btn-secondary'); tabBtnId.classList.add('btn-primary');
|
||||
tabBtnImp.classList.remove('btn-primary'); tabBtnImp.classList.add('btn-secondary');
|
||||
});
|
||||
tabBtnImp.addEventListener('click', function(){
|
||||
tabId.style.display = 'none';
|
||||
tabImp.style.display = 'block';
|
||||
tabBtnImp.classList.remove('btn-secondary'); tabBtnImp.classList.add('btn-primary');
|
||||
tabBtnId.classList.remove('btn-primary'); tabBtnId.classList.add('btn-secondary');
|
||||
// Keep legacy modal ID working for any nav onclick handlers that reference anon-ssh-modal
|
||||
var legacyAlias = document.getElementById('anon-ssh-modal');
|
||||
if (!legacyAlias) {
|
||||
// Create a minimal alias element that delegates to the passkey modal
|
||||
var alias = document.createElement('div');
|
||||
alias.id = 'anon-ssh-modal';
|
||||
alias.style.display = 'none';
|
||||
Object.defineProperty(alias.style, 'display', {
|
||||
set: function(v) { if (v === 'flex' || v === 'block') { var m = document.getElementById('anon-passkey-modal'); if (m) m.style.display = 'flex'; } }
|
||||
});
|
||||
document.body.appendChild(alias);
|
||||
}
|
||||
|
||||
// Copy buttons
|
||||
const copyPubBtn = document.getElementById('anon-copy-pub-btn');
|
||||
if(copyPubBtn){
|
||||
copyPubBtn.addEventListener('click', function(){
|
||||
if(window.f0ckAnonSSH) window.f0ckAnonSSH.copyPublicKey();
|
||||
var modalClose = document.getElementById('anon-passkey-modal-close');
|
||||
if (modalClose) {
|
||||
modalClose.addEventListener('click', function() {
|
||||
var m = document.getElementById('anon-passkey-modal');
|
||||
if (m) m.style.display = 'none';
|
||||
});
|
||||
}
|
||||
const copyFpBtn = document.getElementById('anon-copy-fp-btn');
|
||||
if(copyFpBtn){
|
||||
copyFpBtn.addEventListener('click', function(){
|
||||
const fp = document.getElementById('anon-ssh-fp-display')?.textContent;
|
||||
if(fp && navigator.clipboard){
|
||||
navigator.clipboard.writeText(fp).then(function(){
|
||||
if(typeof window.showToastNotification === 'function') window.showToastNotification('Fingerprint copied!');
|
||||
});
|
||||
}
|
||||
});
|
||||
var overlay = document.getElementById('anon-passkey-modal');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.style.display = 'none'; });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
|
||||
// Download buttons
|
||||
const dlPriv = document.getElementById('anon-dl-priv-btn');
|
||||
if(dlPriv){
|
||||
dlPriv.addEventListener('click', function(){
|
||||
if(window.f0ckAnonSSH) window.f0ckAnonSSH.downloadPrivateKey();
|
||||
});
|
||||
}
|
||||
const dlPub = document.getElementById('anon-dl-pub-btn');
|
||||
if(dlPub){
|
||||
dlPub.addEventListener('click', function(){
|
||||
if(window.f0ckAnonSSH) window.f0ckAnonSSH.downloadPublicKey();
|
||||
});
|
||||
}
|
||||
@if(enable_anonymous_access)
|
||||
<!-- Anonymous Passkey Setup Modal — shown when user clicks "Login as anonymous" -->
|
||||
<div id="anon-setup-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 99999; align-items: center; justify-content: center;">
|
||||
<div style="max-width: 420px; width: 92vw; padding: 28px; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; background: var(--bg-primary, #111); box-shadow: 0 12px 48px rgba(0,0,0,0.9); position: relative; text-align: center;">
|
||||
<button id="anon-setup-modal-close" style="position: absolute; top: 14px; right: 14px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.3em; cursor: pointer; line-height: 1;">×</button>
|
||||
|
||||
// File upload trigger
|
||||
const uploadBtn = document.getElementById('anon-upload-key-btn');
|
||||
const fileElem = document.getElementById('anon-import-file-elem');
|
||||
const inputElem = document.getElementById('anon-import-key-input');
|
||||
if(uploadBtn && fileElem){
|
||||
uploadBtn.addEventListener('click', function(){ fileElem.click(); });
|
||||
fileElem.addEventListener('change', function(){
|
||||
if(fileElem.files && fileElem.files[0]){
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e){
|
||||
if(inputElem) inputElem.value = e.target.result;
|
||||
};
|
||||
reader.readAsText(fileElem.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
<!-- New user view (no passkey yet) -->
|
||||
<div id="anon-setup-new">
|
||||
<div style="font-size: 2.4em; margin-bottom: 12px;">🔑</div>
|
||||
<h3 style="margin: 0 0 10px; font-size: 1.15em; color: var(--text-color, #fff);">Create an anonymous passkey</h3>
|
||||
<p style="margin: 0 0 18px; font-size: 0.88em; color: var(--text-muted, #aaa); line-height: 1.55;">
|
||||
Your browser will ask you to save a <strong>passkey</strong> — a secure credential stored in Bitwarden, iCloud Keychain, or your OS.
|
||||
No account, no email, no password. Your comments and favorites are tied to this passkey.
|
||||
</p>
|
||||
<p style="margin: 0 0 20px; font-size: 0.82em; color: var(--text-muted, #888); line-height: 1.4;">
|
||||
You can use it across devices if your passkey manager syncs (e.g. Bitwarden).
|
||||
</p>
|
||||
<button type="button" id="anon-setup-create-btn" style="width: 100%; padding: 11px; background: var(--accent, #0096ff); color: #fff; border: none; border-radius: 5px; font-size: 0.95em; font-weight: 600; cursor: pointer; margin-bottom: 10px;">
|
||||
<i class="fa-solid fa-fingerprint"></i> Create my passkey
|
||||
</button>
|
||||
<div id="anon-setup-new-error" style="display: none; color: #e06c75; font-size: 0.83em; margin-top: 6px;"></div>
|
||||
<button type="button" id="anon-setup-switch-existing" style="background: none; border: none; color: var(--text-muted, #888); font-size: 0.8em; cursor: pointer; text-decoration: underline; margin-top: 4px;">
|
||||
Already have one? Use existing passkey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
// Submit import
|
||||
const submitImpBtn = document.getElementById('anon-submit-import-btn');
|
||||
const statusEl = document.getElementById('anon-import-status');
|
||||
if(submitImpBtn){
|
||||
submitImpBtn.addEventListener('click', async function(){
|
||||
const keyVal = inputElem?.value;
|
||||
if(!keyVal || !keyVal.trim()){
|
||||
if(statusEl){ statusEl.style.display = 'block'; statusEl.style.color = '#ff4444'; statusEl.textContent = 'Please paste a key or select a file'; }
|
||||
return;
|
||||
}
|
||||
try {
|
||||
submitImpBtn.disabled = true;
|
||||
submitImpBtn.textContent = 'Importing...';
|
||||
await window.f0ckAnonSSH.importKey(keyVal);
|
||||
if(statusEl){
|
||||
statusEl.style.display = 'block';
|
||||
statusEl.style.color = '#00C851';
|
||||
statusEl.textContent = 'Key activated successfully! Reloading...';
|
||||
}
|
||||
setTimeout(function(){ window.location.reload(); }, 800);
|
||||
} catch(err){
|
||||
submitImpBtn.disabled = false;
|
||||
submitImpBtn.innerHTML = '<i class="fa-solid fa-check"></i> Activate Key';
|
||||
if(statusEl){
|
||||
statusEl.style.display = 'block';
|
||||
statusEl.style.color = '#ff4444';
|
||||
statusEl.textContent = 'Import error: ' + (err.message || err);
|
||||
}
|
||||
}
|
||||
});
|
||||
<!-- Returning user view (has passkey) -->
|
||||
<div id="anon-setup-returning" style="display: none;">
|
||||
<div style="font-size: 2.4em; margin-bottom: 12px;">👤</div>
|
||||
<h3 style="margin: 0 0 10px; font-size: 1.15em; color: var(--text-color, #fff);">Use your anonymous passkey</h3>
|
||||
<p style="margin: 0 0 20px; font-size: 0.88em; color: var(--text-muted, #aaa); line-height: 1.55;">
|
||||
Pick your saved passkey from Bitwarden, iCloud Keychain, or your OS to continue as the same anonymous user.
|
||||
</p>
|
||||
<button type="button" id="anon-setup-auth-btn" style="width: 100%; padding: 11px; background: var(--accent, #0096ff); color: #fff; border: none; border-radius: 5px; font-size: 0.95em; font-weight: 600; cursor: pointer; margin-bottom: 10px;">
|
||||
<i class="fa-solid fa-fingerprint"></i> Use my passkey
|
||||
</button>
|
||||
<div id="anon-setup-ret-error" style="display: none; color: #e06c75; font-size: 0.83em; margin-top: 6px;"></div>
|
||||
<button type="button" id="anon-setup-switch-new" style="background: none; border: none; color: var(--text-muted, #888); font-size: 0.8em; cursor: pointer; text-decoration: underline; margin-top: 4px;">
|
||||
Create a new passkey instead
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
function closeSetupModal() {
|
||||
var m = document.getElementById('anon-setup-modal'); if (m) m.style.display = 'none';
|
||||
}
|
||||
var closeBtn = document.getElementById('anon-setup-modal-close');
|
||||
if (closeBtn) closeBtn.addEventListener('click', closeSetupModal);
|
||||
var overlay = document.getElementById('anon-setup-modal');
|
||||
if (overlay) overlay.addEventListener('click', function(e) { if (e.target === overlay) closeSetupModal(); });
|
||||
var switchToExisting = document.getElementById('anon-setup-switch-existing');
|
||||
var switchToNew = document.getElementById('anon-setup-switch-new');
|
||||
if (switchToExisting) switchToExisting.addEventListener('click', function() {
|
||||
document.getElementById('anon-setup-new').style.display = 'none';
|
||||
document.getElementById('anon-setup-returning').style.display = '';
|
||||
});
|
||||
if (switchToNew) switchToNew.addEventListener('click', function() {
|
||||
document.getElementById('anon-setup-returning').style.display = 'none';
|
||||
document.getElementById('anon-setup-new').style.display = '';
|
||||
});
|
||||
var createBtn = document.getElementById('anon-setup-create-btn');
|
||||
if (createBtn) createBtn.addEventListener('click', async function() {
|
||||
var errEl = document.getElementById('anon-setup-new-error');
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
createBtn.disabled = true;
|
||||
createBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving passkey...';
|
||||
try {
|
||||
await window.f0ckAnonPasskey.doRegister();
|
||||
} catch(e) {
|
||||
if (errEl && e && e.name !== 'NotAllowedError') { errEl.style.display = ''; errEl.textContent = e.message || 'Something went wrong.'; }
|
||||
createBtn.disabled = false;
|
||||
createBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Create my passkey';
|
||||
}
|
||||
});
|
||||
var authBtn = document.getElementById('anon-setup-auth-btn');
|
||||
if (authBtn) authBtn.addEventListener('click', async function() {
|
||||
var errEl = document.getElementById('anon-setup-ret-error');
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
authBtn.disabled = true;
|
||||
authBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Waiting for passkey...';
|
||||
try {
|
||||
await window.f0ckAnonPasskey.doAuthenticate();
|
||||
} catch(e) {
|
||||
if (errEl && e && e.name !== 'NotAllowedError') { errEl.style.display = ''; errEl.textContent = e.message || 'Authentication failed.'; }
|
||||
authBtn.disabled = false;
|
||||
authBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey';
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user