cm_bot_v2/web/lib/clipboard.ts
yiekheng 9eed051916 feat(web,api): row-level search, more sort columns, copy buttons
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>
2026-05-04 09:26:11 +08:00

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);
}
}