diff --git a/frontends/web/static/js/auth.js b/frontends/web/static/js/auth.js new file mode 100644 index 0000000..a04edf6 --- /dev/null +++ b/frontends/web/static/js/auth.js @@ -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 = ''; + 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(); +} diff --git a/frontends/web/static/js/generate.js b/frontends/web/static/js/generate.js new file mode 100644 index 0000000..72c0d11 --- /dev/null +++ b/frontends/web/static/js/generate.js @@ -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 `${word}`; + }, + + /** + * 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(' — '); + } + + 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(` + +
+