Skip to content

Commit 9105de5

Browse files
committed
fix: address PR review comments
Security fixes: - Replace templ.SafeURL with templ.URL for mailto links to prevent XSS Copy-clipboard improvements: - Add guard to prevent duplicate event listener attachment - Clear timeout on rapid clicks to prevent icon state issues - Add visual feedback for fallback copy method SVG fixes: - Remove conflicting width/height attributes from copyIcon and checkIcon (CSS classes h-4 w-4 already control sizing) Template improvements: - Add Copyable component to DN on home page for consistency
1 parent 59459ab commit 9105de5

File tree

4 files changed

+56
-19
lines changed

4 files changed

+56
-19
lines changed

internal/web/static/js/copy-clipboard.js

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,76 @@ export function initCopyButtons() {
1616
* @param {HTMLElement} element - The copyable container element
1717
*/
1818
function initCopyable(element) {
19+
// Prevent duplicate initialization
20+
if (element.hasAttribute("data-copy-initialized")) {
21+
return;
22+
}
23+
element.setAttribute("data-copy-initialized", "true");
24+
1925
const button = element.querySelector("[data-copy-button]");
2026
const textElement = element.querySelector("[data-copy-text]");
2127
const copyIcon = element.querySelector("[data-copy-icon]");
2228
const checkIcon = element.querySelector("[data-check-icon]");
2329

2430
if (!button || !textElement) return;
2531

32+
// Store timeout ID for cleanup on rapid clicks
33+
let resetTimeout = null;
34+
2635
button.addEventListener("click", async () => {
2736
const text = textElement.textContent?.trim() || "";
2837

38+
// Clear any pending timeout from previous click
39+
if (resetTimeout) {
40+
clearTimeout(resetTimeout);
41+
resetTimeout = null;
42+
}
43+
2944
try {
3045
await navigator.clipboard.writeText(text);
31-
32-
// Show success feedback
33-
if (copyIcon && checkIcon) {
34-
copyIcon.classList.add("hidden");
35-
checkIcon.classList.remove("hidden");
36-
37-
// Reset after 2 seconds
38-
setTimeout(() => {
39-
copyIcon.classList.remove("hidden");
40-
checkIcon.classList.add("hidden");
41-
}, 2000);
42-
}
46+
showSuccessFeedback(copyIcon, checkIcon, (timeoutId) => {
47+
resetTimeout = timeoutId;
48+
});
4349
} catch (err) {
4450
console.error("Failed to copy text:", err);
4551
// Fallback for older browsers
46-
fallbackCopy(text);
52+
const success = fallbackCopy(text);
53+
if (success) {
54+
showSuccessFeedback(copyIcon, checkIcon, (timeoutId) => {
55+
resetTimeout = timeoutId;
56+
});
57+
}
4758
}
4859
});
4960
}
5061

62+
/**
63+
* Show success feedback by toggling icons.
64+
* @param {HTMLElement|null} copyIcon - The copy icon element
65+
* @param {HTMLElement|null} checkIcon - The check icon element
66+
* @param {function} onTimeout - Callback to store the timeout ID
67+
*/
68+
function showSuccessFeedback(copyIcon, checkIcon, onTimeout) {
69+
if (copyIcon && checkIcon) {
70+
copyIcon.classList.add("hidden");
71+
checkIcon.classList.remove("hidden");
72+
73+
// Reset after 2 seconds
74+
const timeoutId = setTimeout(() => {
75+
copyIcon.classList.remove("hidden");
76+
checkIcon.classList.add("hidden");
77+
}, 2000);
78+
79+
if (onTimeout) {
80+
onTimeout(timeoutId);
81+
}
82+
}
83+
}
84+
5185
/**
5286
* Fallback copy method for browsers without Clipboard API.
5387
* @param {string} text - The text to copy
88+
* @returns {boolean} - Whether the copy was successful
5489
*/
5590
function fallbackCopy(text) {
5691
const textarea = document.createElement("textarea");
@@ -59,10 +94,12 @@ function fallbackCopy(text) {
5994
textarea.style.opacity = "0";
6095
document.body.appendChild(textarea);
6196
textarea.select();
97+
let success = false;
6298
try {
63-
document.execCommand("copy");
99+
success = document.execCommand("copy");
64100
} catch (err) {
65101
console.error("Fallback copy failed:", err);
66102
}
67103
document.body.removeChild(textarea);
104+
return success;
68105
}

internal/web/templates/icons.templ

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,14 @@ templ sparklesIcon() {
170170
}
171171

172172
templ copyIcon() {
173-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" class="inline-block h-4 w-4">
173+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="inline-block h-4 w-4">
174174
<path d="M7 3.5A1.5 1.5 0 0 1 8.5 2h3.879a1.5 1.5 0 0 1 1.06.44l3.122 3.12A1.5 1.5 0 0 1 17 6.622V12.5a1.5 1.5 0 0 1-1.5 1.5h-1v-3.379a3 3 0 0 0-.879-2.121L10.5 5.379A3 3 0 0 0 8.379 4.5H7v-1Z"></path>
175175
<path d="M4.5 6A1.5 1.5 0 0 0 3 7.5v9A1.5 1.5 0 0 0 4.5 18h7a1.5 1.5 0 0 0 1.5-1.5v-5.879a1.5 1.5 0 0 0-.44-1.06L9.44 6.439A1.5 1.5 0 0 0 8.378 6H4.5Z"></path>
176176
</svg>
177177
}
178178

179179
templ checkIcon() {
180-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" class="inline-block h-4 w-4">
180+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="inline-block h-4 w-4">
181181
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd"></path>
182182
</svg>
183183
}

internal/web/templates/index.templ

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ templ Index(user *ldap_cache.FullLDAPUser) {
1515
<span class="text-text-secondary">CN: </span> @Code(user.CN())
1616
</p>
1717
<p>
18-
<span class="text-text-secondary">DN: </span> @Code(user.DN())
18+
<span class="text-text-secondary">DN: </span> @Copyable(user.DN())
1919
</p>
2020
<p>
2121
<span class="text-text-secondary">sAMAccountName: </span> @Code(user.SAMAccountName)
2222
</p>
2323
if user.Mail != nil && *user.Mail != "" {
2424
<p>
2525
<span class="text-text-secondary">Email: </span>
26-
<a href={ templ.SafeURL("mailto:" + *user.Mail) } class="text-accent hover:underline">
26+
<a href={ templ.URL("mailto:" + *user.Mail) } class="text-accent hover:underline">
2727
{ *user.Mail }
2828
</a>
2929
</p>

internal/web/templates/users.templ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ templ User(user *ldap_cache.FullLDAPUser, unassignedGroups []ldap.Group, flashes
4242
<div class="rounded-md border border-border bg-surface-elevated px-3 py-2">
4343
<span class="text-sm text-text-secondary">Email</span>
4444
<p class="font-medium">
45-
<a href={ templ.SafeURL("mailto:" + *user.Mail) } class="text-accent hover:underline">
45+
<a href={ templ.URL("mailto:" + *user.Mail) } class="text-accent hover:underline">
4646
{ *user.Mail }
4747
</a>
4848
</p>

0 commit comments

Comments
 (0)