Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
279 lines
10 KiB
JavaScript
279 lines
10 KiB
JavaScript
// License Management UI
|
|
(function() {
|
|
// Inject license modal HTML
|
|
injectModal('license-modal', `<div id="license-modal" class="weather-modal">
|
|
<div class="weather-modal-content" style="min-width: 420px; max-width: 520px;">
|
|
<h3>DashCaddy License</h3>
|
|
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 16px;">
|
|
Activate a license code to unlock premium features.
|
|
</p>
|
|
|
|
<div id="license-status-section" style="margin-bottom: 16px;">
|
|
<div id="license-badge" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; margin-bottom: 12px;">
|
|
<span id="license-badge-icon"></span>
|
|
<span id="license-badge-text"></span>
|
|
</div>
|
|
<div id="license-details" style="font-size: 0.85rem; color: var(--muted); line-height: 1.6;"></div>
|
|
</div>
|
|
|
|
<div id="license-activate-section">
|
|
<label class="form-label-bold">License Code:</label>
|
|
<input type="text" id="license-code-input" placeholder="DC-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
|
|
maxlength="35" spellcheck="false" autocomplete="off"
|
|
style="width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 4px; background: var(--card-bg); color: var(--fg); font-size: 1rem; font-family: monospace; letter-spacing: 1px;" />
|
|
<p class="tiny-hint">Enter your license code to activate premium features</p>
|
|
</div>
|
|
|
|
<div id="license-features" style="margin-top: 16px;">
|
|
<label class="form-label-bold" style="margin-bottom: 8px; display: block;">Premium Features:</label>
|
|
<div id="license-feature-list" style="display: grid; gap: 8px;"></div>
|
|
</div>
|
|
|
|
<div id="license-error" style="display: none; margin-top: 12px; padding: 10px; border-radius: 4px; background: rgba(231,76,60,0.15); color: var(--bad-fg); font-size: 0.85rem;"></div>
|
|
<div id="license-success" style="display: none; margin-top: 12px; padding: 10px; border-radius: 4px; background: rgba(46,204,113,0.15); color: var(--ok-fg); font-size: 0.85rem;"></div>
|
|
|
|
<div class="weather-modal-buttons" style="margin-top: 16px;">
|
|
<button id="license-cancel">Close</button>
|
|
<button id="license-deactivate" class="btn-accent" style="display: none; background: var(--bad-bg); border-color: var(--bad-fg); color: var(--bad-fg);">Deactivate</button>
|
|
<button id="license-activate" class="btn-accent">Activate</button>
|
|
</div>
|
|
</div>
|
|
</div>`);
|
|
|
|
const modal = document.getElementById('license-modal');
|
|
const codeInput = document.getElementById('license-code-input');
|
|
const activateBtn = document.getElementById('license-activate');
|
|
const deactivateBtn = document.getElementById('license-deactivate');
|
|
const errorEl = document.getElementById('license-error');
|
|
const successEl = document.getElementById('license-success');
|
|
const badgeIcon = document.getElementById('license-badge-icon');
|
|
const badgeText = document.getElementById('license-badge-text');
|
|
const badge = document.getElementById('license-badge');
|
|
const detailsEl = document.getElementById('license-details');
|
|
const featureList = document.getElementById('license-feature-list');
|
|
const activateSection = document.getElementById('license-activate-section');
|
|
|
|
let currentStatus = null;
|
|
|
|
function hideMessages() {
|
|
errorEl.style.display = 'none';
|
|
successEl.style.display = 'none';
|
|
}
|
|
|
|
function showError(msg) {
|
|
hideMessages();
|
|
errorEl.textContent = msg;
|
|
errorEl.style.display = 'block';
|
|
}
|
|
|
|
function showSuccess(msg) {
|
|
hideMessages();
|
|
successEl.textContent = msg;
|
|
successEl.style.display = 'block';
|
|
}
|
|
|
|
function renderStatus(status) {
|
|
currentStatus = status;
|
|
|
|
if (status.active) {
|
|
badge.style.background = 'rgba(46,204,113,0.15)';
|
|
badge.style.color = 'var(--ok-fg)';
|
|
badgeIcon.textContent = '\u2605';
|
|
badgeText.textContent = 'Premium Active';
|
|
const expiryLine = status.lifetime
|
|
? '<div>License: <strong>LIFETIME</strong></div>'
|
|
: `<div>Expires: <strong>${new Date(status.expiresAt).toLocaleDateString()}</strong> (${status.daysRemaining} days remaining)</div>`;
|
|
detailsEl.innerHTML = `
|
|
<div>Code: <code style="font-family: monospace;">${status.code || '***'}</code></div>
|
|
${expiryLine}
|
|
`;
|
|
activateSection.style.display = 'none';
|
|
activateBtn.style.display = 'none';
|
|
deactivateBtn.style.display = '';
|
|
} else {
|
|
badge.style.background = 'rgba(149,165,166,0.15)';
|
|
badge.style.color = 'var(--muted)';
|
|
badgeIcon.textContent = '\u2606';
|
|
badgeText.textContent = status.expired ? 'License Expired' : 'Free Tier';
|
|
detailsEl.innerHTML = status.expired
|
|
? '<div>Your license has expired. Enter a new code to renew.</div>'
|
|
: '<div>Enter a license code to unlock premium features.</div>';
|
|
activateSection.style.display = '';
|
|
activateBtn.style.display = '';
|
|
deactivateBtn.style.display = 'none';
|
|
}
|
|
|
|
// Render feature list
|
|
const features = status.premiumFeatures || {};
|
|
const activeFeatures = new Set(status.features || []);
|
|
featureList.innerHTML = Object.entries(features).map(([key, info]) => {
|
|
const active = activeFeatures.has(key);
|
|
return `<div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border);">
|
|
<span style="font-size: 1.1rem;">${active ? '\u2705' : '\uD83D\uDD12'}</span>
|
|
<div>
|
|
<div style="font-weight: 600; font-size: 0.9rem;">${info.name}</div>
|
|
<div style="font-size: 0.78rem; color: var(--muted);">${info.description}</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
const resp = await fetch('/api/v1/license/status');
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
renderStatus(data.license);
|
|
updateHeaderBadge(data.license);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load license status:', e.message);
|
|
}
|
|
}
|
|
|
|
async function activateLicense() {
|
|
const code = codeInput.value.trim();
|
|
if (!code) {
|
|
showError('Please enter a license code.');
|
|
return;
|
|
}
|
|
|
|
hideMessages();
|
|
activateBtn.disabled = true;
|
|
activateBtn.textContent = 'Activating...';
|
|
|
|
try {
|
|
const resp = await secureFetch('/api/v1/license/activate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code })
|
|
});
|
|
const data = await resp.json();
|
|
|
|
if (data.success) {
|
|
showSuccess(data.message);
|
|
codeInput.value = '';
|
|
renderStatus(data.license);
|
|
showNotification('License activated! Premium features unlocked.', 'success', 5000);
|
|
updateHeaderBadge(data.license);
|
|
} else {
|
|
showError(data.error || 'Activation failed');
|
|
}
|
|
} catch (e) {
|
|
showError('Network error: ' + e.message);
|
|
} finally {
|
|
activateBtn.disabled = false;
|
|
activateBtn.textContent = 'Activate';
|
|
}
|
|
}
|
|
|
|
async function deactivateLicense() {
|
|
if (!confirm('Deactivate your license? You can reuse the code on another machine.')) return;
|
|
|
|
deactivateBtn.disabled = true;
|
|
deactivateBtn.textContent = 'Deactivating...';
|
|
|
|
try {
|
|
const resp = await secureFetch('/api/v1/license/deactivate', { method: 'POST' });
|
|
const data = await resp.json();
|
|
|
|
if (data.success) {
|
|
showSuccess(data.message);
|
|
await loadStatus();
|
|
showNotification('License deactivated.', 'info', 3000);
|
|
updateHeaderBadge({ active: false });
|
|
} else {
|
|
showError(data.error || 'Deactivation failed');
|
|
}
|
|
} catch (e) {
|
|
showError('Network error: ' + e.message);
|
|
} finally {
|
|
deactivateBtn.disabled = false;
|
|
deactivateBtn.textContent = 'Deactivate';
|
|
}
|
|
}
|
|
|
|
function updateHeaderBadge(status) {
|
|
const topbar = document.getElementById('license-status-topbar');
|
|
const iconEl = document.getElementById('license-topbar-icon');
|
|
const textEl = document.getElementById('license-topbar-text');
|
|
const timeEl = document.getElementById('license-topbar-time');
|
|
if (!topbar) return;
|
|
|
|
topbar.className = 'license-status-topbar ' + (status.active ? 'premium' : 'free');
|
|
|
|
if (status.active) {
|
|
iconEl.textContent = '\u2605';
|
|
textEl.textContent = 'PREMIUM';
|
|
if (status.lifetime) {
|
|
timeEl.textContent = '\u00b7 LIFETIME';
|
|
} else {
|
|
const days = status.daysRemaining;
|
|
timeEl.textContent = days != null ? '\u00b7 ' + days + 'd remaining' : '';
|
|
}
|
|
} else {
|
|
iconEl.textContent = '\u2606';
|
|
textEl.textContent = status.expired ? 'EXPIRED' : 'FREE TIER';
|
|
timeEl.textContent = '';
|
|
}
|
|
}
|
|
|
|
function openLicenseModal() {
|
|
hideMessages();
|
|
loadStatus();
|
|
modal.classList.add('show');
|
|
}
|
|
|
|
// Format code input as user types (auto-add dashes)
|
|
codeInput.addEventListener('input', function() {
|
|
let val = this.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
|
|
// Don't auto-format if user is deleting
|
|
if (val.length > this._prevLength) {
|
|
val = val.replace(/-/g, '');
|
|
if (val.length > 2 && !val.startsWith('DC')) {
|
|
val = 'DC' + val;
|
|
}
|
|
// Add dashes after DC and every 5 chars
|
|
if (val.startsWith('DC') && val.length > 2) {
|
|
const parts = ['DC'];
|
|
const rest = val.substring(2);
|
|
for (let i = 0; i < rest.length; i += 5) {
|
|
parts.push(rest.substring(i, i + 5));
|
|
}
|
|
val = parts.join('-');
|
|
}
|
|
}
|
|
this._prevLength = val.length;
|
|
this.value = val;
|
|
});
|
|
|
|
// Wire up events
|
|
activateBtn.addEventListener('click', activateLicense);
|
|
deactivateBtn.addEventListener('click', deactivateLicense);
|
|
codeInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') activateLicense();
|
|
});
|
|
wireModal(modal, document.getElementById('license-cancel'));
|
|
const topbarEl = document.getElementById('license-status-topbar');
|
|
if (topbarEl) topbarEl.addEventListener('click', () => window.openLicenseModal && window.openLicenseModal());
|
|
|
|
// Expose for other modules to open
|
|
window.openLicenseModal = openLicenseModal;
|
|
|
|
// Expose feature check for frontend gating
|
|
window.checkPremiumFeature = async function(feature) {
|
|
try {
|
|
const resp = await fetch(`/api/v1/license/feature/${feature}`);
|
|
const data = await resp.json();
|
|
return data.available;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Load status on page load
|
|
loadStatus().then(status => {
|
|
if (currentStatus) updateHeaderBadge(currentStatus);
|
|
});
|
|
})();
|