fix(paths): prevent duplicate repeater IDs in path

Blocks adding the same hop prefix twice via all three methods:
- List picker: shows warning notification, ignores click
- Map picker: shows warning notification, keeps selection
- Manual entry: validates on Add Path, rejects with error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-23 09:19:55 +01:00
parent 796fb917e4
commit ba26b3dc3a

View File

@@ -1849,6 +1849,18 @@ function setupPathFormHandlers(pubkey) {
return; return;
} }
// Check for duplicate hops in manually entered path
const chunk = hashSize * 2;
const hops = [];
for (let i = 0; i < pathHex.length; i += chunk) {
hops.push(pathHex.substring(i, i + chunk).toLowerCase());
}
const dupes = hops.filter((h, i) => hops.indexOf(h) !== i);
if (dupes.length > 0) {
showNotification(`Duplicate hop(s): ${[...new Set(dupes)].map(d => d.toUpperCase()).join(', ')}`, 'danger');
return;
}
try { try {
const response = await fetch(`/api/contacts/${encodeURIComponent(pubkey)}/paths`, { const response = await fetch(`/api/contacts/${encodeURIComponent(pubkey)}/paths`, {
method: 'POST', method: 'POST',
@@ -1993,6 +2005,12 @@ function renderRepeaterList(listEl, repeaters, pubkey) {
${samePrefix > 1 ? '<i class="bi bi-exclamation-triangle text-warning" title="' + samePrefix + ' repeaters share this prefix"></i>' : ''} ${samePrefix > 1 ? '<i class="bi bi-exclamation-triangle text-warning" title="' + samePrefix + ' repeaters share this prefix"></i>' : ''}
`; `;
item.addEventListener('click', () => { item.addEventListener('click', () => {
// Check for duplicate hop
const existingHops = getCurrentPathHops(hashSize);
if (existingHops.includes(prefix.toLowerCase())) {
showNotification(`${prefix} is already in the path`, 'warning');
return;
}
// Append hop to path hex input // Append hop to path hex input
const current = hexInput.value.replace(/[,\s→]/g, '').trim(); const current = hexInput.value.replace(/[,\s→]/g, '').trim();
const newVal = current + prefix.toLowerCase(); const newVal = current + prefix.toLowerCase();
@@ -2021,6 +2039,21 @@ function filterRepeaterList() {
} }
} }
/**
* Get the list of hop prefixes currently in the path hex input.
*/
function getCurrentPathHops(hashSize) {
const hexInput = document.getElementById('dmPathHexInput');
if (!hexInput) return [];
const rawHex = hexInput.value.replace(/[,\s→]/g, '').trim().toLowerCase();
const chunk = hashSize * 2;
const hops = [];
for (let i = 0; i < rawHex.length; i += chunk) {
hops.push(rawHex.substring(i, i + chunk));
}
return hops;
}
function checkUniquenessWarning(repeaters, hashSize) { function checkUniquenessWarning(repeaters, hashSize) {
const warningEl = document.getElementById('dmPathUniquenessWarning'); const warningEl = document.getElementById('dmPathUniquenessWarning');
if (!warningEl) return; if (!warningEl) return;
@@ -2101,6 +2134,12 @@ function openRepeaterMapPicker() {
if (!_rptMapSelectedRepeater) return; if (!_rptMapSelectedRepeater) return;
const hashSize = parseInt(document.querySelector('input[name="pathHashSize"]:checked').value); const hashSize = parseInt(document.querySelector('input[name="pathHashSize"]:checked').value);
const prefix = _rptMapSelectedRepeater.public_key.substring(0, hashSize * 2).toLowerCase(); const prefix = _rptMapSelectedRepeater.public_key.substring(0, hashSize * 2).toLowerCase();
// Check for duplicate hop
const existingHops = getCurrentPathHops(hashSize);
if (existingHops.includes(prefix)) {
showNotification(`${prefix.toUpperCase()} is already in the path`, 'warning');
return;
}
const hexInput = document.getElementById('dmPathHexInput'); const hexInput = document.getElementById('dmPathHexInput');
if (hexInput) { if (hexInput) {
const current = hexInput.value.replace(/[,\s→]/g, '').trim(); const current = hexInput.value.replace(/[,\s→]/g, '').trim();