Six fixes, each with a test: 1. Character pools are built as arrays of code points, so astral characters (emoji) are never split into lone surrogates. Entropy counts code points. 2. Boolean options are parsed strictly: true/false, 1/0, "true"/"false", "1"/"0", "yes"/"no", "on"/"off" (case-insensitive). Anything else is a 400 naming the option instead of a silent truthy fallback. 3. customSymbols is validated: at most 64 code points, duplicates dropped, control characters and whitespace rejected with a 400. 4. Random mode entropy with requireEach now counts only the passwords that contain a character of every enabled class (inclusion-exclusion). The value is an upper bound and is labelled as such in the API (entropyApproximate/entropyNote) and in the UI. 5. docker-compose.yml uses expose instead of ports, so it no longer collides with Dokploy's own port 3000. 6. SIGTERM/SIGINT shut the server down gracefully: close the listener, let in-flight requests finish, force-exit after 5s. Measured at ~60ms. The server now exports createServer() so the API can be tested over HTTP without binding a fixed port.
243 lines
4.9 KiB
CSS
243 lines
4.9 KiB
CSS
:root {
|
|
--bg: #fafafa;
|
|
--fg: #1a1a1a;
|
|
--muted: #666;
|
|
--line: #ddd;
|
|
--code-bg: #f0f0f0;
|
|
--link: #0b5cad;
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #111;
|
|
--fg: #e6e6e6;
|
|
--muted: #999;
|
|
--line: #333;
|
|
--code-bg: #1c1c1c;
|
|
--link: #c4a7ff;
|
|
}
|
|
}
|
|
|
|
:root[data-theme="dark"] {
|
|
--bg: #111;
|
|
--fg: #e6e6e6;
|
|
--muted: #999;
|
|
--line: #333;
|
|
--code-bg: #1c1c1c;
|
|
--link: #c4a7ff;
|
|
color-scheme: dark;
|
|
}
|
|
|
|
:root[data-theme="light"] { color-scheme: light; }
|
|
|
|
[hidden] { display: none !important; }
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
main {
|
|
max-width: 42rem;
|
|
margin: 0 auto;
|
|
padding: 3rem 1.25rem;
|
|
}
|
|
|
|
h1 { font-size: 1.75rem; margin: 0 0 2rem; }
|
|
|
|
h2 {
|
|
font-size: .8rem;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin: 2.5rem 0 .75rem;
|
|
}
|
|
|
|
a { color: var(--link); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
|
|
.topbar { display: flex; justify-content: flex-end; margin: -1.5rem 0 1.5rem; }
|
|
.theme-toggle {
|
|
padding: .25rem .7rem;
|
|
background: transparent;
|
|
color: var(--fg);
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
font-family: inherit;
|
|
font-size: .85rem;
|
|
line-height: 1.4;
|
|
cursor: pointer;
|
|
}
|
|
.theme-toggle:hover { border-color: var(--muted); }
|
|
.theme-toggle:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
|
|
|
|
dl {
|
|
display: grid;
|
|
grid-template-columns: max-content 1fr;
|
|
gap: .5rem 1.5rem;
|
|
margin: 0;
|
|
}
|
|
dt { color: var(--muted); }
|
|
dd { margin: 0; overflow-wrap: anywhere; }
|
|
|
|
.key { margin: 0 0 1.25rem; }
|
|
.key p { margin: 0 0 .35rem; color: var(--muted); font-size: .9rem; }
|
|
|
|
pre {
|
|
margin: 0;
|
|
padding: .75rem 1rem;
|
|
background: var(--code-bg);
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
}
|
|
|
|
main.error h2 { margin-top: 0; }
|
|
main.error h1 { margin-bottom: .75rem; }
|
|
main.error p { color: var(--muted); margin: 0 0 2rem; }
|
|
|
|
.lead { margin: -1.5rem 0 0; color: var(--muted); }
|
|
|
|
.hint { margin: -.25rem 0 1.25rem; color: var(--muted); font-size: .85rem; }
|
|
|
|
.radios { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; }
|
|
|
|
.radio,
|
|
.check {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .5rem;
|
|
font-size: .95rem;
|
|
}
|
|
|
|
.radio input,
|
|
.check input {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
margin: 0;
|
|
accent-color: var(--link);
|
|
}
|
|
|
|
.checks { display: grid; gap: .4rem; margin-bottom: 1rem; }
|
|
|
|
.field { margin-bottom: 1rem; }
|
|
.field > label {
|
|
display: block;
|
|
margin-bottom: .3rem;
|
|
color: var(--muted);
|
|
font-size: .9rem;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="number"],
|
|
select {
|
|
width: 100%;
|
|
max-width: 24rem;
|
|
padding: .4rem .55rem;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
font: inherit;
|
|
font-size: .95rem;
|
|
}
|
|
|
|
select { cursor: pointer; }
|
|
|
|
input[type="range"] {
|
|
width: 100%;
|
|
max-width: 24rem;
|
|
accent-color: var(--link);
|
|
}
|
|
|
|
.range-row { display: flex; align-items: center; gap: .75rem; }
|
|
.range-row output {
|
|
min-width: 2.5rem;
|
|
color: var(--muted);
|
|
font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
}
|
|
|
|
input:focus-visible,
|
|
select:focus-visible,
|
|
button:focus-visible {
|
|
outline: 2px solid var(--link);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.button {
|
|
padding: .45rem .9rem;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
font-family: inherit;
|
|
font-size: .9rem;
|
|
line-height: 1.4;
|
|
cursor: pointer;
|
|
}
|
|
.button:hover { border-color: var(--muted); }
|
|
|
|
.button.primary {
|
|
background: var(--link);
|
|
color: #fff;
|
|
border-color: var(--link);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) .button.primary { color: #111; }
|
|
}
|
|
:root[data-theme="dark"] .button.primary { color: #111; }
|
|
|
|
.actions { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1.25rem 0; }
|
|
|
|
.error-line {
|
|
margin: 0 0 1.25rem;
|
|
padding-left: .6rem;
|
|
border-left: 2px solid var(--link);
|
|
color: var(--fg);
|
|
font-size: .9rem;
|
|
}
|
|
|
|
.pw { margin-bottom: 1.5rem; }
|
|
.pw:last-child { margin-bottom: 0; }
|
|
|
|
.pw-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: .5rem .9rem;
|
|
margin-top: .5rem;
|
|
color: var(--muted);
|
|
font-size: .85rem;
|
|
}
|
|
|
|
.pw-entropy { font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
|
|
.strength { display: inline-flex; gap: 3px; }
|
|
.strength i {
|
|
display: inline-block;
|
|
width: 14px;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background: var(--line);
|
|
}
|
|
.strength i.on { background: var(--link); }
|
|
|
|
.pw-strength-label { text-transform: capitalize; }
|
|
|
|
@media (max-width: 30rem) {
|
|
dl { grid-template-columns: 1fr; gap: 0; }
|
|
dd { margin-bottom: .75rem; }
|
|
input[type="text"],
|
|
input[type="number"],
|
|
select,
|
|
input[type="range"] { max-width: 100%; }
|
|
.actions .button { flex: 1 1 auto; }
|
|
}
|