Refactor: Extract inline JS to external files

New JS files:
- auth.js: Password toggle, confirmation validation, copy, regenerate
- generate.js: Form controls, credential display, memory story generation

Updated templates to use external JS:
- login.html, setup.html, account.html
- admin/user_new.html, user_created.html, password_reset.html
- generate.html (now uses generate.js + minimal Jinja-dependent inline)

Core stegasoo.js (943 lines) unchanged - already handles encode/decode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-03 23:33:17 -05:00
parent e502f42fb8
commit f4c1aa1912
9 changed files with 442 additions and 295 deletions

View File

@@ -0,0 +1,142 @@
/**
* Stegasoo Authentication Pages JavaScript
* Handles login, setup, account, and admin user management pages
*/
const StegasooAuth = {
// ========================================================================
// PASSWORD VISIBILITY TOGGLE
// ========================================================================
/**
* Toggle password field visibility
* @param {string} inputId - ID of the password input
* @param {HTMLElement} btn - The toggle button element
*/
togglePassword(inputId, btn) {
const input = document.getElementById(inputId);
const icon = btn.querySelector('i');
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
icon?.classList.replace('bi-eye', 'bi-eye-slash');
} else {
input.type = 'password';
icon?.classList.replace('bi-eye-slash', 'bi-eye');
}
},
// ========================================================================
// PASSWORD CONFIRMATION VALIDATION
// ========================================================================
/**
* Initialize password confirmation validation on a form
* @param {string} formId - ID of the form
* @param {string} passwordId - ID of the password field
* @param {string} confirmId - ID of the confirmation field
*/
initPasswordConfirmation(formId, passwordId, confirmId) {
const form = document.getElementById(formId);
if (!form) return;
form.addEventListener('submit', function(e) {
const password = document.getElementById(passwordId)?.value;
const confirm = document.getElementById(confirmId)?.value;
if (password !== confirm) {
e.preventDefault();
alert('Passwords do not match');
}
});
},
// ========================================================================
// COPY TO CLIPBOARD
// ========================================================================
/**
* Copy field value to clipboard with visual feedback
* @param {string} fieldId - ID of the input field to copy
*/
copyField(fieldId) {
const field = document.getElementById(fieldId);
if (!field) return;
field.select();
navigator.clipboard.writeText(field.value).then(() => {
const btn = field.nextElementSibling;
if (!btn) return;
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check"></i>';
setTimeout(() => btn.innerHTML = originalHTML, 1000);
});
},
// ========================================================================
// PASSWORD GENERATION
// ========================================================================
/**
* Generate a random password
* @param {number} length - Password length (default 8)
* @returns {string} Generated password
*/
generatePassword(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let password = '';
for (let i = 0; i < length; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
},
/**
* Regenerate password and update input field
* @param {string} inputId - ID of the password input
* @param {number} length - Password length
*/
regeneratePassword(inputId = 'passwordInput', length = 8) {
const input = document.getElementById(inputId);
if (input) {
input.value = this.generatePassword(length);
}
},
// ========================================================================
// DELETE CONFIRMATION
// ========================================================================
/**
* Confirm deletion with a prompt
* @param {string} itemName - Name of item being deleted
* @param {string} formId - ID of the form to submit if confirmed
* @returns {boolean} True if confirmed
*/
confirmDelete(itemName, formId = null) {
const confirmed = confirm(`Are you sure you want to delete "${itemName}"? This cannot be undone.`);
if (confirmed && formId) {
const form = document.getElementById(formId);
form?.submit();
}
return confirmed;
}
};
// Make togglePassword available globally for onclick handlers
function togglePassword(inputId, btn) {
StegasooAuth.togglePassword(inputId, btn);
}
// Make copyField available globally for onclick handlers
function copyField(fieldId) {
StegasooAuth.copyField(fieldId);
}
// Make regeneratePassword available globally for onclick handlers
function regeneratePassword() {
StegasooAuth.regeneratePassword();
}

View File

@@ -0,0 +1,285 @@
/**
* Stegasoo Generate Page JavaScript
* Handles credential generation form and display
*/
const StegasooGenerate = {
// ========================================================================
// FORM CONTROLS
// ========================================================================
/**
* Initialize the words range slider
*/
initWordsSlider() {
const wordsRange = document.getElementById('wordsRange');
const wordsValue = document.getElementById('wordsValue');
wordsRange?.addEventListener('input', function() {
const bits = this.value * 11;
wordsValue.textContent = `${this.value} words (~${bits} bits)`;
});
},
/**
* Initialize PIN/RSA option toggles
*/
initOptionToggles() {
const usePinCheck = document.getElementById('usePinCheck');
const useRsaCheck = document.getElementById('useRsaCheck');
const pinOptions = document.getElementById('pinOptions');
const rsaOptions = document.getElementById('rsaOptions');
const rsaQrWarning = document.getElementById('rsaQrWarning');
const rsaBitsSelect = document.getElementById('rsaBitsSelect');
usePinCheck?.addEventListener('change', function() {
pinOptions?.classList.toggle('d-none', !this.checked);
});
useRsaCheck?.addEventListener('change', function() {
rsaOptions?.classList.toggle('d-none', !this.checked);
});
// RSA key size QR warning (>3072 bits)
rsaBitsSelect?.addEventListener('change', function() {
rsaQrWarning?.classList.toggle('d-none', parseInt(this.value) <= 3072);
});
},
// ========================================================================
// CREDENTIAL VISIBILITY
// ========================================================================
pinHidden: false,
passphraseHidden: false,
/**
* Toggle PIN visibility
*/
togglePinVisibility() {
const pinDigits = document.getElementById('pinDigits');
const icon = document.getElementById('pinToggleIcon');
const text = document.getElementById('pinToggleText');
this.pinHidden = !this.pinHidden;
pinDigits?.classList.toggle('blurred', this.pinHidden);
if (icon) icon.className = this.pinHidden ? 'bi bi-eye' : 'bi bi-eye-slash';
if (text) text.textContent = this.pinHidden ? 'Show' : 'Hide';
},
/**
* Toggle passphrase visibility
*/
togglePassphraseVisibility() {
const display = document.getElementById('passphraseDisplay');
const icon = document.getElementById('passphraseToggleIcon');
const text = document.getElementById('passphraseToggleText');
this.passphraseHidden = !this.passphraseHidden;
display?.classList.toggle('blurred', this.passphraseHidden);
if (icon) icon.className = this.passphraseHidden ? 'bi bi-eye' : 'bi bi-eye-slash';
if (text) text.textContent = this.passphraseHidden ? 'Show' : 'Hide';
},
// ========================================================================
// MEMORY AID STORY GENERATION
// ========================================================================
currentStoryTemplate: 0,
/**
* Story templates organized by word count (3-12 words supported)
*/
storyTemplates: {
3: [
w => `The ${w[0]} ${w[1]} ${w[2]}.`,
w => `${w[0]} loves ${w[1]} and ${w[2]}.`,
w => `A ${w[0]} found a ${w[1]} near the ${w[2]}.`,
w => `${w[0]}, ${w[1]}, ${w[2]} — never forget.`,
w => `The ${w[0]} hid the ${w[1]} under the ${w[2]}.`,
],
4: [
w => `${w[0]} and ${w[1]} discovered a ${w[2]} made of ${w[3]}.`,
w => `The ${w[0]} ${w[1]} ate ${w[2]} for ${w[3]}.`,
w => `In the ${w[0]}, a ${w[1]} met a ${w[2]} carrying ${w[3]}.`,
w => `${w[0]} said "${w[1]}" while holding a ${w[2]} ${w[3]}.`,
w => `The secret: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}.`,
],
5: [
w => `${w[0]} traveled to ${w[1]} seeking the ${w[2]} of ${w[3]} and ${w[4]}.`,
w => `The ${w[0]} ${w[1]} lived in a ${w[2]} house with ${w[3]} ${w[4]}.`,
w => `"${w[0]}!" shouted ${w[1]} as the ${w[2]} ${w[3]} flew toward ${w[4]}.`,
w => `Captain ${w[0]} sailed the ${w[1]} ${w[2]} searching for ${w[3]} ${w[4]}.`,
w => `In ${w[0]} kingdom, ${w[1]} guards protected the ${w[2]} ${w[3]} ${w[4]}.`,
],
6: [
w => `${w[0]} met ${w[1]} at the ${w[2]}. Together they found ${w[3]}, ${w[4]}, and ${w[5]}.`,
w => `The ${w[0]} ${w[1]} wore a ${w[2]} hat while eating ${w[3]} ${w[4]} ${w[5]}.`,
w => `Detective ${w[0]} found ${w[1]} ${w[2]} near the ${w[3]} ${w[4]} ${w[5]}.`,
w => `In the ${w[0]} ${w[1]}, a ${w[2]} ${w[3]} sang about ${w[4]} ${w[5]}.`,
w => `Chef ${w[0]} combined ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, and ${w[5]}.`,
],
7: [
w => `${w[0]} and ${w[1]} walked through the ${w[2]} ${w[3]} to find the ${w[4]} ${w[5]} ${w[6]}.`,
w => `The ${w[0]} professor studied ${w[1]} ${w[2]} while drinking ${w[3]} ${w[4]} with ${w[5]} ${w[6]}.`,
w => `"${w[0]} ${w[1]}!" yelled ${w[2]} as ${w[3]} ${w[4]} attacked the ${w[5]} ${w[6]}.`,
w => `In ${w[0]}, King ${w[1]} decreed that ${w[2]} ${w[3]} must honor ${w[4]} ${w[5]} ${w[6]}.`,
],
8: [
w => `${w[0]} ${w[1]} and ${w[2]} ${w[3]} met at the ${w[4]} ${w[5]} to discuss ${w[6]} ${w[7]}.`,
w => `The ${w[0]} ${w[1]} ${w[2]} traveled from ${w[3]} to ${w[4]} carrying ${w[5]} ${w[6]} ${w[7]}.`,
w => `${w[0]} discovered that ${w[1]} ${w[2]} plus ${w[3]} ${w[4]} equals ${w[5]} ${w[6]} ${w[7]}.`,
],
9: [
w => `${w[0]} ${w[1]} ${w[2]} watched as ${w[3]} ${w[4]} ${w[5]} danced with ${w[6]} ${w[7]} ${w[8]}.`,
w => `In the ${w[0]} ${w[1]} ${w[2]}, three friends — ${w[3]}, ${w[4]}, ${w[5]} — found ${w[6]} ${w[7]} ${w[8]}.`,
w => `The recipe: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, ${w[5]}, ${w[6]}, ${w[7]}, ${w[8]}.`,
],
10: [
w => `${w[0]} ${w[1]} told ${w[2]} ${w[3]} about the ${w[4]} ${w[5]} ${w[6]} hidden in ${w[7]} ${w[8]} ${w[9]}.`,
w => `The ${w[0]} ${w[1]} ${w[2]} ${w[3]} ${w[4]} lived beside ${w[5]} ${w[6]} ${w[7]} ${w[8]} ${w[9]}.`,
],
11: [
w => `${w[0]} ${w[1]} ${w[2]} and ${w[3]} ${w[4]} ${w[5]} discovered ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]}.`,
w => `In ${w[0]} ${w[1]}, the ${w[2]} ${w[3]} ${w[4]} sang of ${w[5]} ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]}.`,
],
12: [
w => `${w[0]} ${w[1]} ${w[2]} met ${w[3]} ${w[4]} ${w[5]} at the ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]} ${w[11]}.`,
w => `The twelve treasures: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, ${w[5]}, ${w[6]}, ${w[7]}, ${w[8]}, ${w[9]}, ${w[10]}, ${w[11]}.`,
],
},
/**
* Wrap word in highlight span
*/
hl(word) {
return `<span class="passphrase-word">${word}</span>`;
},
/**
* Generate a memory story for given words
* @param {string[]} words - Array of passphrase words
* @param {number|null} idx - Template index (null for current)
* @returns {string} HTML story
*/
generateStory(words, idx = null) {
const count = words.length;
if (count === 0) return '';
// Clamp to supported range (3-12)
const templateKey = Math.max(3, Math.min(12, count));
const templates = this.storyTemplates[templateKey];
if (!templates || templates.length === 0) {
// Fallback: just list the words
return words.map(w => this.hl(w)).join(' &mdash; ');
}
const templateIdx = (idx ?? this.currentStoryTemplate) % templates.length;
// Apply highlighting to words
const highlighted = words.map(w => this.hl(w));
return templates[templateIdx](highlighted);
},
/**
* Toggle memory aid visibility
* @param {string[]} words - Passphrase words array
*/
toggleMemoryAid(words) {
const container = document.getElementById('memoryAidContainer');
const icon = document.getElementById('memoryAidIcon');
const text = document.getElementById('memoryAidText');
const isHidden = container?.classList.contains('d-none');
container?.classList.toggle('d-none', !isHidden);
if (icon) icon.className = isHidden ? 'bi bi-lightbulb-fill' : 'bi bi-lightbulb';
if (text) text.textContent = isHidden ? 'Hide Aid' : 'Memory Aid';
if (isHidden) {
document.getElementById('memoryStory').innerHTML = this.generateStory(words);
}
},
/**
* Regenerate story with next template
* @param {string[]} words - Passphrase words array
*/
regenerateStory(words) {
const count = words.length;
const templateKey = Math.max(3, Math.min(12, count));
const templates = this.storyTemplates[templateKey] || [];
this.currentStoryTemplate = (this.currentStoryTemplate + 1) % Math.max(1, templates.length);
document.getElementById('memoryStory').innerHTML = this.generateStory(words, this.currentStoryTemplate);
},
// ========================================================================
// QR CODE PRINTING
// ========================================================================
/**
* Print QR code in new window
*/
printQrCode() {
const qrImg = document.getElementById('qrCodeImage');
if (!qrImg) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`<!DOCTYPE html>
<html>
<head>
<title>Stegasoo RSA Key QR Code</title>
<style>
body { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; font-family: sans-serif; }
img { max-width: 400px; }
.warning { margin-top: 20px; padding: 10px; border: 2px solid #ff9800; background: #fff3e0; max-width: 400px; text-align: center; font-size: 12px; }
</style>
</head>
<body>
<h2>Stegasoo RSA Private Key</h2>
<img src="${qrImg.src}" alt="RSA Key QR Code">
<div class="warning">
<strong>Warning:</strong> This QR code contains your unencrypted RSA private key.
Store securely and destroy after use.
</div>
<script>window.onload = function() { window.print(); }<\/script>
</body>
</html>`);
printWindow.document.close();
},
// ========================================================================
// INITIALIZATION
// ========================================================================
/**
* Initialize generate form page
*/
initForm() {
this.initWordsSlider();
this.initOptionToggles();
}
};
// Global function wrappers for onclick handlers
function togglePinVisibility() {
StegasooGenerate.togglePinVisibility();
}
function togglePassphraseVisibility() {
StegasooGenerate.togglePassphraseVisibility();
}
function printQrCode() {
StegasooGenerate.printQrCode();
}
// Auto-init form controls
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('[data-page="generate"]')) {
StegasooGenerate.initForm();
}
});