fix: service edit, CSRF token stability, and license restore (v1.1.1)
- Fix service edit double-write bug (was creating duplicate entries) - Add editable display name field to service edit modal - Backend update endpoint now accepts name, logo, and recalculates url - Fix CSRF token regeneration breaking all POST requests (nonce was being regenerated on every request, invalidating cached tokens) - CSRF nonce now persists across requests, rotated only on TOTP login - Frontend secureFetch auto-retries on CSRF failure with fresh token - Restore lifetime license activation on DNS2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
const modal = document.getElementById('service-edit-modal');
|
||||
|
||||
document.getElementById('service-edit-title').textContent = `Edit ${service.name}`;
|
||||
document.getElementById('edit-service-name-display').textContent = service.name;
|
||||
document.getElementById('edit-service-name').value = service.name;
|
||||
document.getElementById('edit-service-url-display').textContent = service.url || buildServiceUrl(service.id);
|
||||
document.getElementById('edit-service-logo-preview').src = service.logo || `/assets/${service.id}.png`;
|
||||
document.getElementById('edit-subdomain').value = service.id;
|
||||
@@ -31,6 +31,7 @@
|
||||
if (!currentEditService) return;
|
||||
|
||||
const newSubdomain = document.getElementById('edit-subdomain').value.trim().toLowerCase();
|
||||
const newName = document.getElementById('edit-service-name').value.trim();
|
||||
const newPort = document.getElementById('edit-port').value.trim();
|
||||
const newIp = document.getElementById('edit-ip').value.trim() || 'localhost';
|
||||
const tailscaleOnly = document.getElementById('edit-tailscale-only').checked;
|
||||
@@ -45,10 +46,11 @@
|
||||
const changes = [];
|
||||
|
||||
if (newSubdomain !== oldSubdomain) changes.push('subdomain');
|
||||
if (newName && newName !== currentEditService.name) changes.push('name');
|
||||
if (newPort && newPort !== String(currentEditService.port)) changes.push('port');
|
||||
if (newIp !== currentEditService.ip) changes.push('ip');
|
||||
if (tailscaleOnly !== (currentEditService.tailscaleOnly || false)) changes.push('tailscale');
|
||||
if (newLogo !== currentEditService.logo) changes.push('logo');
|
||||
if (newLogo && newLogo !== currentEditService.logo) changes.push('logo');
|
||||
|
||||
if (changes.length === 0) {
|
||||
closeServiceEditModal();
|
||||
@@ -60,31 +62,32 @@
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
if (changes.includes('subdomain') || changes.includes('port') || changes.includes('ip') || changes.includes('tailscale')) {
|
||||
const response = await secureFetch('/api/v1/services/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
oldSubdomain,
|
||||
newSubdomain,
|
||||
port: newPort || currentEditService.port,
|
||||
ip: newIp,
|
||||
tailscaleOnly
|
||||
})
|
||||
});
|
||||
const response = await secureFetch('/api/v1/services/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
oldSubdomain,
|
||||
newSubdomain,
|
||||
name: newName || currentEditService.name,
|
||||
port: newPort || currentEditService.port,
|
||||
ip: newIp,
|
||||
tailscaleOnly,
|
||||
logo: newLogo || undefined
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update service');
|
||||
}
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update service');
|
||||
}
|
||||
|
||||
// Update local APPS array
|
||||
// Update local APPS array to match
|
||||
const appIndex = window.APPS.findIndex(a => a.id === oldSubdomain);
|
||||
if (appIndex !== -1) {
|
||||
window.APPS[appIndex] = {
|
||||
...window.APPS[appIndex],
|
||||
id: newSubdomain,
|
||||
name: newName || window.APPS[appIndex].name,
|
||||
port: newPort || window.APPS[appIndex].port,
|
||||
ip: newIp,
|
||||
tailscaleOnly,
|
||||
@@ -92,27 +95,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Update services via API
|
||||
await secureFetch('/api/v1/services', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: newSubdomain,
|
||||
name: currentEditService.name,
|
||||
port: newPort || currentEditService.port,
|
||||
ip: newIp,
|
||||
logo: newLogo || currentEditService.logo,
|
||||
tailscaleOnly,
|
||||
containerId: currentEditService.containerId,
|
||||
appTemplate: currentEditService.appTemplate
|
||||
})
|
||||
});
|
||||
|
||||
// If subdomain changed, remove old entry
|
||||
if (newSubdomain !== oldSubdomain) {
|
||||
await secureFetch(`/api/v1/services/${oldSubdomain}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
closeServiceEditModal();
|
||||
window.buildGrid();
|
||||
window.refreshAll();
|
||||
|
||||
Reference in New Issue
Block a user