reze: Add Node.js password generator with memorable and random modes
Public password generator with no auth and no npm dependencies. - Server-side generation using crypto.randomInt; two levels: diceware-style memorable passphrases (42abacus-91abdomen) and fully random characters. - Configurable word list (EFF Large / EFF Short), digits per word, separators, capitalization, length, character class toggles, custom symbols and ambiguous-character exclusion. - Light/dark theme matching druid520/site, entropy estimate and strength bar, clipboard copy, options persisted in localStorage. - Dockerfile for Dokploy plus a Woodpecker deploy hook on trunk.
This commit is contained in:
+379
@@ -0,0 +1,379 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'opts';
|
||||
|
||||
var DEFAULTS = {
|
||||
mode: 'memorable',
|
||||
words: 4,
|
||||
digits: 2,
|
||||
digitSeparator: '',
|
||||
separator: '-',
|
||||
capitalization: 'none',
|
||||
list: 'eff-large',
|
||||
length: 20,
|
||||
useLower: true,
|
||||
useUpper: true,
|
||||
useDigits: true,
|
||||
useSymbols: false,
|
||||
customSymbols: '',
|
||||
excludeAmbiguous: false,
|
||||
requireEach: true,
|
||||
count: 1
|
||||
};
|
||||
|
||||
var FIELDS = [
|
||||
{ key: 'words', id: 'words', type: 'int', min: 2, max: 10 },
|
||||
{ key: 'digits', id: 'digits', type: 'int', min: 0, max: 4 },
|
||||
{ key: 'digitSeparator', id: 'digitSeparator', type: 'str' },
|
||||
{ key: 'separator', id: 'separator', type: 'str' },
|
||||
{ key: 'capitalization', id: 'capitalization', type: 'str' },
|
||||
{ key: 'list', id: 'list', type: 'str' },
|
||||
{ key: 'length', id: 'length', type: 'int', min: 8, max: 64 },
|
||||
{ key: 'count', id: 'count', type: 'int', min: 1, max: 10 },
|
||||
{ key: 'customSymbols', id: 'customSymbols', type: 'str' },
|
||||
{ key: 'useLower', id: 'useLower', type: 'bool' },
|
||||
{ key: 'useUpper', id: 'useUpper', type: 'bool' },
|
||||
{ key: 'useDigits', id: 'useDigits', type: 'bool' },
|
||||
{ key: 'useSymbols', id: 'useSymbols', type: 'bool' },
|
||||
{ key: 'excludeAmbiguous', id: 'excludeAmbiguous', type: 'bool' },
|
||||
{ key: 'requireEach', id: 'requireEach', type: 'bool' }
|
||||
];
|
||||
|
||||
var STRENGTH_LABELS = ['very weak', 'weak', 'fair', 'strong', 'very strong'];
|
||||
var STRENGTH_THRESHOLDS = [28, 40, 60, 80];
|
||||
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function clampInt(value, fallback, min, max) {
|
||||
var n = parseInt(value, 10);
|
||||
if (isNaN(n)) return fallback;
|
||||
if (n < min) return min;
|
||||
if (n > max) return max;
|
||||
return n;
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
var state = {};
|
||||
var key;
|
||||
for (key in DEFAULTS) {
|
||||
if (Object.prototype.hasOwnProperty.call(DEFAULTS, key)) state[key] = DEFAULTS[key];
|
||||
}
|
||||
try {
|
||||
var raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
var saved = JSON.parse(raw);
|
||||
if (saved && typeof saved === 'object') {
|
||||
for (key in saved) {
|
||||
if (Object.prototype.hasOwnProperty.call(state, key)) state[key] = saved[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return state;
|
||||
}
|
||||
|
||||
function saveState(state) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function readState() {
|
||||
var state = {};
|
||||
var i;
|
||||
var field;
|
||||
var el;
|
||||
|
||||
for (i = 0; i < FIELDS.length; i += 1) {
|
||||
field = FIELDS[i];
|
||||
el = byId(field.id);
|
||||
if (!el) continue;
|
||||
if (field.type === 'bool') {
|
||||
state[field.key] = el.checked;
|
||||
} else if (field.type === 'int') {
|
||||
state[field.key] = clampInt(el.value, DEFAULTS[field.key], field.min, field.max);
|
||||
} else {
|
||||
state[field.key] = el.value;
|
||||
}
|
||||
}
|
||||
|
||||
var checked = document.querySelector('input[name="mode"]:checked');
|
||||
state.mode = checked ? checked.value : DEFAULTS.mode;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function applyState(state) {
|
||||
var i;
|
||||
var field;
|
||||
var el;
|
||||
|
||||
for (i = 0; i < FIELDS.length; i += 1) {
|
||||
field = FIELDS[i];
|
||||
el = byId(field.id);
|
||||
if (!el) continue;
|
||||
if (field.type === 'bool') {
|
||||
el.checked = Boolean(state[field.key]);
|
||||
} else if (field.type === 'int') {
|
||||
el.value = clampInt(state[field.key], DEFAULTS[field.key], field.min, field.max);
|
||||
} else {
|
||||
el.value = state[field.key] === undefined || state[field.key] === null ? '' : state[field.key];
|
||||
}
|
||||
}
|
||||
|
||||
var radios = document.querySelectorAll('input[name="mode"]');
|
||||
for (i = 0; i < radios.length; i += 1) {
|
||||
radios[i].checked = radios[i].value === state.mode;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePanels(state) {
|
||||
var memorable = byId('panel-memorable');
|
||||
var random = byId('panel-random');
|
||||
if (memorable) memorable.hidden = state.mode !== 'memorable';
|
||||
if (random) random.hidden = state.mode !== 'random';
|
||||
}
|
||||
|
||||
function updateLengthOut() {
|
||||
var length = byId('length');
|
||||
var out = byId('length-out');
|
||||
if (length && out) out.textContent = length.value;
|
||||
}
|
||||
|
||||
function strengthLevel(bits) {
|
||||
var i;
|
||||
for (i = 0; i < STRENGTH_THRESHOLDS.length; i += 1) {
|
||||
if (bits < STRENGTH_THRESHOLDS[i]) return i;
|
||||
}
|
||||
return STRENGTH_LABELS.length - 1;
|
||||
}
|
||||
|
||||
function buildStrength(bits) {
|
||||
var level = strengthLevel(bits);
|
||||
var wrap = document.createElement('span');
|
||||
wrap.className = 'strength';
|
||||
wrap.setAttribute('role', 'img');
|
||||
wrap.setAttribute('aria-label', 'strength: ' + STRENGTH_LABELS[level]);
|
||||
var i;
|
||||
for (i = 0; i < STRENGTH_LABELS.length; i += 1) {
|
||||
var seg = document.createElement('i');
|
||||
if (i <= level) seg.className = 'on';
|
||||
wrap.appendChild(seg);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
var box = byId('error');
|
||||
if (!box) return;
|
||||
if (message) {
|
||||
box.textContent = message;
|
||||
box.hidden = false;
|
||||
} else {
|
||||
box.textContent = '';
|
||||
box.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function copyWithFallback(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
var area = document.createElement('textarea');
|
||||
area.value = text;
|
||||
area.setAttribute('readonly', 'readonly');
|
||||
area.style.position = 'absolute';
|
||||
area.style.left = '-9999px';
|
||||
document.body.appendChild(area);
|
||||
area.select();
|
||||
var ok = false;
|
||||
try {
|
||||
ok = document.execCommand('copy');
|
||||
} catch (e) {
|
||||
ok = false;
|
||||
}
|
||||
document.body.removeChild(area);
|
||||
if (ok) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('copy failed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyPassword(value, button) {
|
||||
var original = button.getAttribute('data-label') || 'Copy';
|
||||
copyWithFallback(value).then(function () {
|
||||
button.textContent = 'Copied';
|
||||
}, function () {
|
||||
button.textContent = 'Failed';
|
||||
}).then(function () {
|
||||
window.setTimeout(function () {
|
||||
button.textContent = original;
|
||||
}, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPasswords(data) {
|
||||
var container = byId('results');
|
||||
if (!container) return;
|
||||
container.textContent = '';
|
||||
|
||||
var list = (data && data.passwords) || [];
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
const item = list[i];
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'pw';
|
||||
|
||||
const value = document.createElement('pre');
|
||||
value.className = 'pw-value';
|
||||
value.textContent = item.value;
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'pw-meta';
|
||||
|
||||
const entropy = document.createElement('span');
|
||||
entropy.className = 'pw-entropy';
|
||||
entropy.textContent = item.entropy + ' bits';
|
||||
|
||||
const level = strengthLevel(item.entropy);
|
||||
const label = document.createElement('span');
|
||||
label.className = 'pw-strength-label';
|
||||
label.textContent = STRENGTH_LABELS[level];
|
||||
|
||||
const copy = document.createElement('button');
|
||||
copy.type = 'button';
|
||||
copy.className = 'button copy';
|
||||
copy.textContent = 'Copy';
|
||||
copy.setAttribute('data-label', 'Copy');
|
||||
copy.setAttribute('aria-label', 'Copy password');
|
||||
copy.addEventListener('click', function () {
|
||||
copyPassword(item.value, copy);
|
||||
});
|
||||
|
||||
meta.appendChild(entropy);
|
||||
meta.appendChild(buildStrength(item.entropy));
|
||||
meta.appendChild(label);
|
||||
meta.appendChild(copy);
|
||||
|
||||
row.appendChild(value);
|
||||
row.appendChild(meta);
|
||||
container.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function payloadFor(state) {
|
||||
if (state.mode === 'random') {
|
||||
return {
|
||||
mode: 'random',
|
||||
length: state.length,
|
||||
useLower: state.useLower,
|
||||
useUpper: state.useUpper,
|
||||
useDigits: state.useDigits,
|
||||
useSymbols: state.useSymbols,
|
||||
customSymbols: state.customSymbols,
|
||||
excludeAmbiguous: state.excludeAmbiguous,
|
||||
requireEach: state.requireEach,
|
||||
count: state.count
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: 'memorable',
|
||||
words: state.words,
|
||||
digits: state.digits,
|
||||
digitSeparator: state.digitSeparator,
|
||||
separator: state.separator,
|
||||
capitalization: state.capitalization,
|
||||
list: state.list,
|
||||
count: state.count
|
||||
};
|
||||
}
|
||||
|
||||
function generate() {
|
||||
var state = readState();
|
||||
saveState(state);
|
||||
showError('');
|
||||
|
||||
fetch('/api/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payloadFor(state))
|
||||
}).then(function (response) {
|
||||
return response.json().then(function (data) {
|
||||
if (!response.ok) {
|
||||
throw new Error(data && data.error ? data.error : 'request failed with status ' + response.status);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}).then(function (data) {
|
||||
renderPasswords(data);
|
||||
}, function (err) {
|
||||
showError(err && err.message ? err.message : 'request failed');
|
||||
});
|
||||
}
|
||||
|
||||
function loadMeta() {
|
||||
fetch('/api/meta', { headers: { Accept: 'application/json' } }).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
var lists = (data && data.lists) || [];
|
||||
var i;
|
||||
for (i = 0; i < lists.length; i += 1) {
|
||||
var option = document.querySelector('#list option[value="' + lists[i].id + '"]');
|
||||
if (option) option.textContent = lists[i].name + ' (' + lists[i].size + ' words)';
|
||||
}
|
||||
}, function () {});
|
||||
}
|
||||
|
||||
function init() {
|
||||
var state = loadState();
|
||||
applyState(state);
|
||||
updatePanels(state);
|
||||
updateLengthOut();
|
||||
|
||||
var i;
|
||||
var field;
|
||||
for (i = 0; i < FIELDS.length; i += 1) {
|
||||
field = FIELDS[i];
|
||||
(function (f) {
|
||||
var el = byId(f.id);
|
||||
if (!el) return;
|
||||
var handler = function () {
|
||||
var current = readState();
|
||||
saveState(current);
|
||||
updatePanels(current);
|
||||
updateLengthOut();
|
||||
};
|
||||
el.addEventListener('change', handler);
|
||||
el.addEventListener('input', handler);
|
||||
})(field);
|
||||
}
|
||||
|
||||
var radios = document.querySelectorAll('input[name="mode"]');
|
||||
for (i = 0; i < radios.length; i += 1) {
|
||||
radios[i].addEventListener('change', function () {
|
||||
var current = readState();
|
||||
saveState(current);
|
||||
updatePanels(current);
|
||||
});
|
||||
}
|
||||
|
||||
var generateBtn = byId('generate');
|
||||
if (generateBtn) generateBtn.addEventListener('click', generate);
|
||||
var regenerateBtn = byId('regenerate');
|
||||
if (regenerateBtn) regenerateBtn.addEventListener('click', generate);
|
||||
|
||||
loadMeta();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user