Three operator-quality-of-life features behind the same caching and
pagination contract that the existing tables already use:
- Search bar on /acc and /users (Find/Enter to apply, Clear to reset).
Backed by a new `q` API param that filters via WHERE
username/f_username LIKE 'q%' on both rows + count queries so the
table header total stays consistent under a filter.
- Two more sortable columns: acc.status and user.t_username. Sort
columns are whitelisted because sort_col is f-string'd into ORDER
BY (parameterised binding doesn't apply to column names) — anything
outside the allowed set falls back to the table's default.
- Copy button on every row that writes a multi-line credentials
message to the clipboard. New lib/clipboard.ts helper tries
navigator.clipboard.writeText() first and falls back to
textarea+execCommand("copy") so it works over the internal-network
HTTP deploy where the modern API is gated by secure-context rules.
Acc message: Username/Password (+Link if set). User message:
From/To username and password.
Also: inactive sort indicators now render ↓ (the direction they'll
sort on first click) instead of the more ambiguous ↕.
Test suite grows from 53 to 70: tests/test_user_search_filter.py
(9 tests) pins the q-filter contract on both /user/ and /acc/;
tests/test_sort_whitelist.py (8 tests) pins the allowed sort columns
and proves out-of-set values cannot reach the SQL parser.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
/**
|
|
* Copy `text` to the system clipboard.
|
|
*
|
|
* The modern `navigator.clipboard.writeText()` only works in secure
|
|
* contexts (HTTPS / localhost / file://). This deployment runs over
|
|
* plain HTTP on the internal network, where that API is gated, so we
|
|
* fall back to the legacy `textarea + execCommand("copy")` pattern.
|
|
* `execCommand` is deprecated but every shipping browser still
|
|
* implements it for the copy command, and it works in non-secure
|
|
* contexts where the modern API doesn't.
|
|
*
|
|
* Returns true on success, false if both paths fail.
|
|
*/
|
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch {
|
|
// Fall through to legacy path — secure-context block, permission
|
|
// denial, or any other modern-API failure.
|
|
}
|
|
}
|
|
|
|
if (typeof document === "undefined") return false;
|
|
const ta = document.createElement("textarea");
|
|
ta.value = text;
|
|
ta.setAttribute("readonly", "");
|
|
// Position offscreen but still focusable; some browsers refuse to
|
|
// copy from elements that are display:none or visibility:hidden.
|
|
ta.style.position = "fixed";
|
|
ta.style.top = "0";
|
|
ta.style.left = "0";
|
|
ta.style.opacity = "0";
|
|
ta.style.pointerEvents = "none";
|
|
document.body.appendChild(ta);
|
|
try {
|
|
ta.focus();
|
|
ta.select();
|
|
return document.execCommand("copy");
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
document.body.removeChild(ta);
|
|
}
|
|
}
|