- features/admin/: the 10 admin-owned config controllers, the 5 admin pages
(overview/system/charts/metric/storage), ssh-page.js, peers-page.js, plus
admin.css/ip-whitelist.css/ssh.css (eager). config-manager.js kept last in
the load order (it news the sub-managers).
- shared/js/: config-shared.js + config-options.js (ConfigShared/ConfigOptions
globals consumed cross-feature by backup/apps/tasks).
- shared/css/: forms.css + config.css (generic form + config-form primitives
borrowed by apps/backup/admin).
- Updated all path strings in system-loader.js (config component) and
config-manager.js (lazyLoad of admin/ssh/peers controllers); index.html CSS
hrefs. No /js/components/{config,admin,ssh,peers}/ refs remain.
js/components/ now holds only shared UI (topbar, notifications, eo-modal,
update-notifier, mobile-menu, confirmation-dialog).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
647 lines
24 KiB
JavaScript
Executable File
647 lines
24 KiB
JavaScript
Executable File
// Domain Management - Handles domain-related functionality
|
||
class DomainManager {
|
||
constructor() {
|
||
this.domains = [];
|
||
}
|
||
|
||
async checkTraefikInstallation() {
|
||
try {
|
||
const response = await fetch('/api/traefik/status');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
return data.installed || false;
|
||
}
|
||
return false;
|
||
} catch (error) {
|
||
//console.log('DomainManager: Traefik status check failed, assuming not installed:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async renderDomainsSection(configItems, displaySubcategory, subcategoryDescription) {
|
||
const traefikStatus = await this.checkTraefikInstallation();
|
||
|
||
let html = `
|
||
<div class="config-category">
|
||
<div class="domains-wrapper">
|
||
<div class="domains-header">
|
||
<div>
|
||
<h3>${displaySubcategory}</h3>
|
||
<p class="category-description">${subcategoryDescription}</p>
|
||
</div>
|
||
</div>
|
||
<div class="domains-divider"></div>
|
||
`;
|
||
|
||
if (!traefikStatus.installed) {
|
||
html += `
|
||
<div class="traefik-warning-banner">
|
||
<div class="traefik-warning-content">
|
||
<span class="traefik-warning-icon">⚠️</span>
|
||
<div class="traefik-warning-text">
|
||
<strong>Traefik Not Installed</strong> - Domain settings won't be applied until Traefik is installed. You can configure domains now and install Traefik later.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
html += `<div class="domain-building-blocks">`;
|
||
|
||
// Only show domains that have content (non-empty values)
|
||
const allDomainKeys = configItems.map(item => item.key).filter(key => key.startsWith('CFG_DOMAIN_'));
|
||
const domainKeysWithContent = allDomainKeys.filter(key => {
|
||
const configItem = configItems.find(item => item.key === key) || {};
|
||
const value = configItem.value || '';
|
||
return value.trim() !== '';
|
||
});
|
||
|
||
// Check if we've reached the maximum of 9 domains (count only existing domains, not empty slots)
|
||
const isMaxDomains = domainKeysWithContent.length >= 9;
|
||
|
||
domainKeysWithContent.forEach(key => {
|
||
const configItem = configItems.find(item => item.key === key) || {};
|
||
const value = configItem.value || '';
|
||
const title = configItem.title || this.formatConfigLabel(key);
|
||
const fieldId = `config-${key}`;
|
||
|
||
// Extract domain number
|
||
const domainNum = parseInt(key.match(/CFG_DOMAIN_(\d+)/)[1]);
|
||
const isHighestDomain = domainNum === Math.max(...domainKeysWithContent.map(k =>
|
||
parseInt(k.match(/CFG_DOMAIN_(\d+)/)[1])
|
||
));
|
||
|
||
// Domain 1 can never be deleted, and only highest numbered domain WITH CONTENT can be deleted
|
||
const canDelete = isHighestDomain && domainNum !== 1;
|
||
|
||
html += `
|
||
<div class="domain-building-block">
|
||
<div class="domain-header">
|
||
${this.generateField(fieldId, key, value, title, '', {
|
||
placeholder: 'example.com',
|
||
className: 'domain-input'
|
||
})}
|
||
<button type="button" class="delete-domain-btn ${!canDelete ? 'disabled' : ''}"
|
||
onclick="window.deleteDomain('${key}', this)"
|
||
title="${canDelete ? 'Delete domain' : domainNum === 1 ? 'Domain 1 cannot be deleted' : 'Can only delete highest numbered domain'}"
|
||
${!canDelete ? 'disabled' : ''}>
|
||
<span class="delete-icon">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="setup-dns-status" data-dns-for="${fieldId}"></div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
setTimeout(() => this.attachDnsChecks(), 0);
|
||
|
||
html += `
|
||
</div>
|
||
<div class="domain-actions">
|
||
<button type="button" id="add-domain-btn" class="btn ${isMaxDomains ? 'btn-secondary' : 'btn-primary'}"
|
||
onclick="window.addDomain()"
|
||
${isMaxDomains ? 'disabled' : ''}>
|
||
<span class="add-icon">${isMaxDomains ? '✓' : '+'}</span>
|
||
${isMaxDomains ? 'Maximum Domains Reached' : 'Add Domain'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="spacer spacer-lg"></div>
|
||
</div>
|
||
`;
|
||
|
||
return html;
|
||
}
|
||
|
||
attachDnsChecks() {
|
||
const inputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]');
|
||
inputs.forEach((input) => {
|
||
if (input.dataset.dnsBound === '1') return;
|
||
input.dataset.dnsBound = '1';
|
||
const status = document.querySelector(`[data-dns-for="${input.id}"]`);
|
||
if (!status) return;
|
||
let timer = null;
|
||
let ctrl = null;
|
||
const run = () => {
|
||
if (timer) clearTimeout(timer);
|
||
if (ctrl) ctrl.abort();
|
||
const domain = input.value.trim().toLowerCase();
|
||
if (!domain) { status.textContent = ''; status.className = 'setup-dns-status'; return; }
|
||
if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(domain)) {
|
||
status.textContent = ''; status.className = 'setup-dns-status';
|
||
return;
|
||
}
|
||
status.textContent = 'Checking DNS…';
|
||
status.className = 'setup-dns-status checking';
|
||
timer = setTimeout(async () => {
|
||
ctrl = new AbortController();
|
||
try {
|
||
const res = await fetch(`/api/setup/dns-check?domain=${encodeURIComponent(domain)}`, { signal: ctrl.signal });
|
||
const data = await res.json();
|
||
if (data.matches) {
|
||
status.textContent = `✓ ${domain} → ${data.server_ip} (this server)`;
|
||
status.className = 'setup-dns-status ok';
|
||
} else {
|
||
const detail = data.domain_ip
|
||
? `points to ${data.domain_ip}, this server is ${data.server_ip}`
|
||
: `does not resolve (server: ${data.server_ip || 'unknown'})`;
|
||
status.textContent = `⚠ ${domain} ${detail}. Traefik may not route this yet.`;
|
||
status.className = 'setup-dns-status warn';
|
||
}
|
||
} catch (err) {
|
||
if (err.name !== 'AbortError') {
|
||
status.textContent = 'DNS check failed.';
|
||
status.className = 'setup-dns-status warn';
|
||
}
|
||
}
|
||
}, 500);
|
||
};
|
||
input.addEventListener('input', run);
|
||
input.addEventListener('blur', run);
|
||
if (input.value) run();
|
||
});
|
||
}
|
||
|
||
generateField(fieldId, key, value, title, description, options = {}) {
|
||
// Use the same field generation as ConfigShared
|
||
if (typeof ConfigShared !== 'undefined') {
|
||
return ConfigShared.generateField(fieldId, key, value, title, description, options);
|
||
}
|
||
|
||
// Fallback field generation
|
||
const placeholder = key.startsWith('CFG_DOMAIN_') ? 'example.com' : 'https://example.com';
|
||
return `
|
||
<input type="url" id="${fieldId}" name="${key}" value="${value}" class="form-control" placeholder="${placeholder}">
|
||
`;
|
||
}
|
||
|
||
formatConfigLabel(key) {
|
||
// Special handling for domain configuration
|
||
if (key.startsWith('CFG_DOMAIN_')) {
|
||
const domainNum = key.replace('CFG_DOMAIN_', '');
|
||
return `Domain ${domainNum}`;
|
||
}
|
||
|
||
// Use ConfigShared if available
|
||
if (typeof ConfigShared !== 'undefined') {
|
||
return ConfigShared.formatConfigLabel(key);
|
||
}
|
||
|
||
// Fallback formatting
|
||
return key.replace(/^CFG_/, '').replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
addNewDomain() {
|
||
//console.log('Add Domain button clicked!');
|
||
|
||
try {
|
||
// Find the highest existing domain number
|
||
const domainInputs = document.querySelectorAll('input[name^="CFG_DOMAIN_"]');
|
||
const domainNumbers = [];
|
||
|
||
domainInputs.forEach(input => {
|
||
const match = input.name.match(/CFG_DOMAIN_(\d+)/);
|
||
if (match) {
|
||
domainNumbers.push(parseInt(match[1]));
|
||
}
|
||
});
|
||
|
||
// Check if we've reached the maximum of 9 domains (count only existing domains, not empty slots)
|
||
if (domainNumbers.length >= 9) {
|
||
//console.log('Maximum of 9 domains reached');
|
||
return;
|
||
}
|
||
|
||
const nextDomainNumber = domainNumbers.length > 0 ? Math.max(...domainNumbers) + 1 : 1;
|
||
const newDomainKey = `CFG_DOMAIN_${nextDomainNumber}`;
|
||
const newFieldId = `config-${newDomainKey}`;
|
||
|
||
// Create new domain building block HTML
|
||
const newDomainHTML = `
|
||
<div class="domain-building-block">
|
||
<div class="domain-header">
|
||
${this.generateField(newFieldId, newDomainKey, '', `Domain ${nextDomainNumber}`, '', { placeholder: 'example.com' })}
|
||
<button type="button" class="delete-domain-btn"
|
||
onclick="window.configManager.deleteDomain('${newDomainKey}', this)">
|
||
<span class="delete-icon">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="setup-dns-status" data-dns-for="${newFieldId}"></div>
|
||
</div>
|
||
`;
|
||
|
||
// Find the domain-building-blocks container and add the new block
|
||
const domainContainer = document.querySelector('.domain-building-blocks');
|
||
if (domainContainer) {
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = newDomainHTML;
|
||
const newBlock = tempDiv.firstElementChild;
|
||
domainContainer.appendChild(newBlock);
|
||
|
||
// Focus on the new input field
|
||
const newInput = newBlock.querySelector('input');
|
||
if (newInput) {
|
||
newInput.focus();
|
||
}
|
||
|
||
// Update add domain button state
|
||
this.updateDomainDeleteButtons();
|
||
this.attachDnsChecks();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding new domain:', error);
|
||
}
|
||
}
|
||
|
||
deleteDomain(domainKey, buttonElement) {
|
||
if (typeof window.DomainManager === 'undefined' || !window.DomainManager.deleteDomain) {
|
||
console.error('DomainManager not available for deleteDomain');
|
||
return;
|
||
}
|
||
//console.log(`Delete domain button clicked for: ${domainKey}`);
|
||
|
||
try {
|
||
// Find the domain-building-block and remove it
|
||
const domainBlock = buttonElement.closest('.domain-building-block');
|
||
if (domainBlock) {
|
||
// Clear the input value first
|
||
const input = domainBlock.querySelector('input');
|
||
if (input) {
|
||
input.value = '';
|
||
}
|
||
// Remove the entire building block
|
||
domainBlock.remove();
|
||
|
||
// Update delete button states
|
||
this.updateDomainDeleteButtons();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting domain:', error);
|
||
}
|
||
}
|
||
|
||
updateDomainDeleteButtons() {
|
||
// Find all domain building blocks
|
||
const allDomainBlocks = document.querySelectorAll('.domain-building-block');
|
||
|
||
// Find all domain numbers from existing blocks
|
||
const allDomainNumbers = [];
|
||
allDomainBlocks.forEach(block => {
|
||
const input = block.querySelector('input');
|
||
if (input) {
|
||
const inputName = input.name;
|
||
const match = inputName.match(/CFG_DOMAIN_(\d+)/);
|
||
if (match) {
|
||
allDomainNumbers.push(parseInt(match[1]));
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update delete button states (only highest numbered domain can be deleted, but NEVER Domain 1)
|
||
allDomainBlocks.forEach((block, index) => {
|
||
const deleteBtn = block.querySelector('.delete-domain-btn');
|
||
if (deleteBtn) {
|
||
const input = block.querySelector('input');
|
||
if (input) {
|
||
const inputName = input.name;
|
||
const domainNum = inputName.match(/CFG_DOMAIN_(\d+)/);
|
||
const domainNumber = domainNum ? parseInt(domainNum[1]) : 0;
|
||
|
||
// Find the highest domain number among all visible blocks
|
||
const highestDomainNumber = Math.max(...allDomainNumbers);
|
||
|
||
// Only highest numbered domain can be deleted, but NEVER Domain 1
|
||
const canDelete = domainNumber === highestDomainNumber && domainNumber !== 1;
|
||
|
||
deleteBtn.disabled = !canDelete;
|
||
deleteBtn.className = `delete-domain-btn ${!canDelete ? 'disabled' : ''}`;
|
||
deleteBtn.title = canDelete ? 'Delete domain' : domainNumber === 1 ? 'Domain 1 cannot be deleted' : 'Can only delete highest numbered domain';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Standalone domain management functions - immediately available
|
||
window.addDomain = function() {
|
||
//console.log('Add Domain button clicked!');
|
||
|
||
try {
|
||
// Before adding new domain, validate that all existing domains have valid format
|
||
if (!canAddNewDomain()) {
|
||
// Find the first invalid domain and focus it with flash
|
||
const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]');
|
||
for (const input of allDomainInputs) {
|
||
if (!validateDomainFormat(input, false)) { // Don't show notification here
|
||
input.style.animation = 'flash 0.5s ease-in-out 2';
|
||
input.focus();
|
||
setTimeout(() => {
|
||
input.style.animation = '';
|
||
}, 1000);
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Find the highest existing domain number
|
||
const domainInputs = document.querySelectorAll('input[name^="CFG_DOMAIN_"]');
|
||
const domainNumbers = [];
|
||
|
||
domainInputs.forEach(input => {
|
||
const match = input.name.match(/CFG_DOMAIN_(\d+)/);
|
||
if (match) {
|
||
domainNumbers.push(parseInt(match[1]));
|
||
}
|
||
});
|
||
|
||
// Check if we've reached the maximum of 9 domains
|
||
if (domainNumbers.length >= 9) {
|
||
//console.log('Maximum of 9 domains reached');
|
||
return;
|
||
}
|
||
|
||
const nextDomainNumber = domainNumbers.length > 0 ? Math.max(...domainNumbers) + 1 : 1;
|
||
const newDomainKey = `CFG_DOMAIN_${nextDomainNumber}`;
|
||
const newFieldId = `config-${newDomainKey}`;
|
||
|
||
// Create new domain building block HTML
|
||
const newDomainHTML = `
|
||
<div class="domain-building-block">
|
||
<div class="domain-header">
|
||
${typeof ConfigShared !== 'undefined' ? ConfigShared.generateField(newFieldId, newDomainKey, '', `Domain ${nextDomainNumber}`, '', {
|
||
placeholder: 'example.com',
|
||
className: 'domain-input',
|
||
onchange: 'validateDomainFormat(this, true)',
|
||
oninput: 'validateDomainFormat(this, true)',
|
||
onblur: 'validateDomainFormat(this, true)'
|
||
}) : `<input type="url" id="${newFieldId}" name="${newDomainKey}" value="" class="form-control domain-input" placeholder="example.com" onchange="validateDomainFormat(this, true)" oninput="validateDomainFormat(this, true)" onblur="validateDomainFormat(this, true)">`}
|
||
<button type="button" class="delete-domain-btn"
|
||
onclick="window.deleteDomain('${newDomainKey}', this)">
|
||
<span class="delete-icon">×</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Find the domain-building-blocks container and add the new block
|
||
const domainContainer = document.querySelector('.domain-building-blocks');
|
||
if (domainContainer) {
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = newDomainHTML;
|
||
const newBlock = tempDiv.firstElementChild;
|
||
domainContainer.appendChild(newBlock);
|
||
|
||
// Focus on the new input field
|
||
const newInput = newBlock.querySelector('input');
|
||
if (newInput) {
|
||
newInput.focus();
|
||
}
|
||
|
||
// Update add domain button state
|
||
updateDomainDeleteButtons();
|
||
updateAddDomainButton();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding new domain:', error);
|
||
}
|
||
};
|
||
|
||
window.deleteDomain = function(domainKey, buttonElement) {
|
||
//console.log(`Delete domain button clicked for: ${domainKey}`);
|
||
|
||
try {
|
||
// Find the domain-building-block and remove it
|
||
const domainBlock = buttonElement.closest('.domain-building-block');
|
||
if (domainBlock) {
|
||
// Clear the input value first
|
||
const input = domainBlock.querySelector('input');
|
||
if (input) {
|
||
input.value = '';
|
||
}
|
||
// Remove the entire building block
|
||
domainBlock.remove();
|
||
|
||
// Update add domain button state (re-enable if we're below 9 domains)
|
||
updateDomainDeleteButtons();
|
||
updateAddDomainButton();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting domain:', error);
|
||
}
|
||
};
|
||
|
||
// Helper function to update delete button states
|
||
function updateDomainDeleteButtons() {
|
||
// Find all domain building blocks
|
||
const allDomainBlocks = document.querySelectorAll('.domain-building-block');
|
||
|
||
// Find all domain numbers from existing blocks
|
||
const allDomainNumbers = [];
|
||
allDomainBlocks.forEach(block => {
|
||
const input = block.querySelector('input');
|
||
if (input) {
|
||
const inputName = input.name;
|
||
const match = inputName.match(/CFG_DOMAIN_(\d+)/);
|
||
if (match) {
|
||
allDomainNumbers.push(parseInt(match[1]));
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update delete button states (only highest numbered domain can be deleted, but NEVER Domain 1)
|
||
allDomainBlocks.forEach((block, index) => {
|
||
const deleteBtn = block.querySelector('.delete-domain-btn');
|
||
if (deleteBtn) {
|
||
const input = block.querySelector('input');
|
||
if (input) {
|
||
const inputName = input.name;
|
||
const domainNum = inputName.match(/CFG_DOMAIN_(\d+)/);
|
||
const domainNumber = domainNum ? parseInt(domainNum[1]) : 0;
|
||
|
||
// Find the highest domain number among all visible blocks
|
||
const highestDomainNumber = Math.max(...allDomainNumbers);
|
||
|
||
// Only highest numbered domain can be deleted, but NEVER Domain 1
|
||
const canDelete = domainNumber === highestDomainNumber && domainNumber !== 1;
|
||
|
||
deleteBtn.disabled = !canDelete;
|
||
deleteBtn.className = `delete-domain-btn ${!canDelete ? 'disabled' : ''}`;
|
||
deleteBtn.title = canDelete ? 'Delete domain' : domainNumber === 1 ? 'Domain 1 cannot be deleted' : 'Can only delete highest numbered domain';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Helper function to update add domain button state
|
||
function updateAddDomainButton() {
|
||
const addDomainBtn = document.getElementById('add-domain-btn');
|
||
if (addDomainBtn) {
|
||
// Find all domain building blocks
|
||
const allDomainBlocks = document.querySelectorAll('.domain-building-block');
|
||
|
||
// Find all domain numbers from existing blocks
|
||
const allDomainNumbers = [];
|
||
allDomainBlocks.forEach(block => {
|
||
const input = block.querySelector('input');
|
||
if (input) {
|
||
const inputName = input.name;
|
||
const match = inputName.match(/CFG_DOMAIN_(\d+)/);
|
||
if (match) {
|
||
allDomainNumbers.push(parseInt(match[1]));
|
||
}
|
||
}
|
||
});
|
||
|
||
// Check if we've reached the maximum of 9 domains
|
||
const isMaxDomains = allDomainNumbers.length >= 9;
|
||
|
||
// Update button state
|
||
addDomainBtn.disabled = isMaxDomains;
|
||
addDomainBtn.className = `btn ${isMaxDomains ? 'btn-secondary' : 'btn-primary'}`;
|
||
addDomainBtn.innerHTML = `<span class="add-icon">${isMaxDomains ? '✓' : '+'}</span>${isMaxDomains ? 'Maximum Domains Reached' : 'Add Domain'}`;
|
||
}
|
||
}
|
||
|
||
// Validate domain format when user tries to add a new domain
|
||
function validateDomainFormat(input, showNotifications = true) {
|
||
const value = input.value.trim();
|
||
//console.log('validateDomainFormat called with:', value, 'showNotifications:', showNotifications);
|
||
//console.log('window.notificationSystem available:', !!window.notificationSystem);
|
||
|
||
if (!value) {
|
||
// Clear styling for empty fields
|
||
input.style.borderColor = '#dc3545';
|
||
input.title = 'Domain cannot be empty';
|
||
if (showNotifications && window.notificationSystem) {
|
||
//console.log('Attempting to show empty domain notification');
|
||
window.notificationSystem.error('Domain cannot be empty');
|
||
} else {
|
||
console.error('Domain cannot be empty - notification system not available');
|
||
}
|
||
return false; // Empty fields are not valid for adding new domains
|
||
}
|
||
|
||
// Basic domain validation regex - supports subdomains and multiple TLD levels
|
||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*\.[a-zA-Z]{2,}$/;
|
||
const isValidFormat = domainRegex.test(value);
|
||
|
||
// Check for duplicates
|
||
const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]');
|
||
const duplicates = Array.from(allDomainInputs).filter(otherInput =>
|
||
otherInput !== input && otherInput.value.trim() === value
|
||
);
|
||
const hasDuplicate = duplicates.length > 0;
|
||
|
||
if (!isValidFormat) {
|
||
input.style.borderColor = '#dc3545';
|
||
input.title = 'Invalid domain format (e.g., example.com)';
|
||
if (showNotifications && window.notificationSystem) {
|
||
//console.log('Attempting to show invalid format notification');
|
||
window.notificationSystem.error('Invalid domain format: "' + value + '". Please use a valid domain like example.com');
|
||
} else {
|
||
console.error('Invalid domain format - notification system not available');
|
||
}
|
||
return false;
|
||
} else if (hasDuplicate) {
|
||
input.style.borderColor = '#dc3545';
|
||
input.title = 'Domain already exists';
|
||
if (showNotifications && window.notificationSystem) {
|
||
//console.log('Attempting to show duplicate notification');
|
||
window.notificationSystem.error('Domain "' + value + '" already exists');
|
||
} else {
|
||
console.error('Domain already exists - notification system not available');
|
||
}
|
||
return false;
|
||
} else {
|
||
input.style.borderColor = '';
|
||
input.title = '';
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Check if all domains are valid before allowing new domain addition
|
||
function canAddNewDomain() {
|
||
const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]');
|
||
for (const input of allDomainInputs) {
|
||
const domainValue = input.value.trim();
|
||
|
||
// Check if domain is empty
|
||
if (!domainValue) {
|
||
// Flash the empty input field
|
||
input.style.animation = 'flash 0.5s ease-in-out 2';
|
||
input.focus();
|
||
setTimeout(() => {
|
||
input.style.animation = '';
|
||
}, 1000);
|
||
|
||
// Show notification with safety check
|
||
if (window.notificationSystem) {
|
||
window.notificationSystem.error('Domain cannot be empty');
|
||
} else {
|
||
console.error('Domain cannot be empty');
|
||
}
|
||
return false; // Empty domain - don't allow adding new domain
|
||
}
|
||
|
||
// Check for duplicates with notification (always show)
|
||
if (checkForDuplicateDomain(input, domainValue)) {
|
||
return false;
|
||
}
|
||
|
||
// Check domain format with notification (always show)
|
||
if (checkForInvalidDomainFormat(input, domainValue)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true; // All domains are valid and non-empty
|
||
}
|
||
|
||
// Separate function to check for invalid format that always shows notifications
|
||
function checkForInvalidDomainFormat(input, domainValue) {
|
||
if (!domainValue) return false;
|
||
|
||
// Basic domain validation regex - supports subdomains and multiple TLD levels
|
||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*\.[a-zA-Z]{2,}$/;
|
||
const isValidFormat = domainRegex.test(domainValue);
|
||
|
||
if (!isValidFormat) {
|
||
input.style.borderColor = '#dc3545';
|
||
input.title = 'Invalid domain format (e.g., example.com)';
|
||
if (window.notificationSystem) {
|
||
//console.log('Showing invalid domain format notification');
|
||
window.notificationSystem.error('Invalid domain format: "' + domainValue + '". Please use a valid domain like example.com');
|
||
} else {
|
||
console.error('Invalid domain format - notification system not available');
|
||
}
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// Separate function to check for duplicates that always shows notifications
|
||
function checkForDuplicateDomain(input, domainValue) {
|
||
if (!domainValue) return false;
|
||
|
||
const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]');
|
||
const duplicates = Array.from(allDomainInputs).filter(otherInput =>
|
||
otherInput !== input && otherInput.value.trim() === domainValue
|
||
);
|
||
const hasDuplicate = duplicates.length > 0;
|
||
|
||
if (hasDuplicate) {
|
||
input.style.borderColor = '#dc3545';
|
||
input.title = 'Domain already exists';
|
||
if (window.notificationSystem) {
|
||
//console.log('Showing duplicate domain notification');
|
||
window.notificationSystem.error('Domain "' + domainValue + '" already exists');
|
||
} else {
|
||
console.error('Domain already exists - notification system not available');
|
||
}
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|