Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
278
status/js/license.js
Normal file
278
status/js/license.js
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user