alotta good shit

This commit is contained in:
2026-09-19 06:20:37 +02:00
parent 74f7884525
commit 1477d56658
45 changed files with 4363 additions and 2069 deletions
+272 -165
View File
@@ -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">&times;</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;">&times;</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;">&times;</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-----&#10;...&#10;-----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;">&times;</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