Template
Add optional expiration that starts on first config use.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+90
-25
@@ -109,10 +109,21 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('expiration_date_label') }}</label>
|
||||
<div class="grid" style="display: grid; grid-template-columns: 1fr 120px auto; gap: var(--space-sm); align-items: center;">
|
||||
<label style="display:flex; align-items:center; gap:var(--space-sm); margin-bottom:var(--space-sm); cursor:pointer;">
|
||||
<input type="checkbox" id="newExpireAfterFirstUse" onchange="toggleExpireAfterFirstUse('new')">
|
||||
{{ _('expire_after_first_use') }}
|
||||
</label>
|
||||
<div id="newExpireAbsoluteWrap" class="grid" style="display: grid; grid-template-columns: 1fr 120px auto; gap: var(--space-sm); align-items: center;">
|
||||
<input class="form-input" type="date" id="newExpirationDate" aria-label="Expiration date">
|
||||
<select class="form-select" id="newExpirationTime" aria-label="Expiration time"></select>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="clearDateTimeFields('newExpirationDate', 'newExpirationTime')">Без срока</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="clearDateTimeFields('newExpirationDate', 'newExpirationTime')">{{ _('no_expiration') }}</button>
|
||||
</div>
|
||||
<div id="newExpireDaysWrap" style="display:none;">
|
||||
<div style="display:flex; gap:var(--space-sm); align-items:center;">
|
||||
<input class="form-input" type="number" id="newExpirationDays" min="1" value="30" style="max-width:120px;">
|
||||
<span style="color:var(--text-muted);">{{ _('days_short') }}</span>
|
||||
</div>
|
||||
<div class="form-hint">{{ _('expire_after_first_use_hint') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -224,10 +235,21 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('expiration_date_label') }}</label>
|
||||
<div class="grid" style="display: grid; grid-template-columns: 1fr 120px auto; gap: var(--space-sm); align-items: center;">
|
||||
<label style="display:flex; align-items:center; gap:var(--space-sm); margin-bottom:var(--space-sm); cursor:pointer;">
|
||||
<input type="checkbox" id="editExpireAfterFirstUse" onchange="toggleExpireAfterFirstUse('edit')">
|
||||
{{ _('expire_after_first_use') }}
|
||||
</label>
|
||||
<div id="editExpireAbsoluteWrap" class="grid" style="display: grid; grid-template-columns: 1fr 120px auto; gap: var(--space-sm); align-items: center;">
|
||||
<input class="form-input" type="date" id="editUserExpiration" aria-label="Expiration date">
|
||||
<select class="form-select" id="editUserExpirationTime" aria-label="Expiration time"></select>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="clearDateTimeFields('editUserExpiration', 'editUserExpirationTime')">Без срока</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="clearDateTimeFields('editUserExpiration', 'editUserExpirationTime')">{{ _('no_expiration') }}</button>
|
||||
</div>
|
||||
<div id="editExpireDaysWrap" style="display:none;">
|
||||
<div style="display:flex; gap:var(--space-sm); align-items:center;">
|
||||
<input class="form-input" type="number" id="editExpirationDays" min="1" value="30" style="max-width:120px;">
|
||||
<span style="color:var(--text-muted);">{{ _('days_short') }}</span>
|
||||
</div>
|
||||
<div class="form-hint" id="editExpireDaysHint">{{ _('expire_after_first_use_hint') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -501,7 +523,7 @@
|
||||
${u.created_at ? `<span>📅 ${u.created_at.substring(0, 10)}</span>` : ''}
|
||||
${u.telegramId ? `<span>💬 ${u.telegramId}</span>` : ''}
|
||||
${u.email ? `<span>📧 ${u.email}</span>` : ''}
|
||||
${u.expiration_date ? `<span style="color: ${new Date(u.expiration_date) < new Date() ? 'var(--danger)' : 'var(--text-muted)'}">⌛ ${_('expiration_date_label')}: ${new Date(u.expiration_date).toLocaleString()}</span>` : ''}
|
||||
${formatExpirationBadge(u)}
|
||||
</div>
|
||||
${u.description ? `<div class="client-meta" style="font-size: 0.75rem; opacity: 0.8; margin-top: 2px;">📝 ${u.description}</div>` : ''}
|
||||
${(() => {
|
||||
@@ -545,7 +567,7 @@
|
||||
onclick="openAddConnectionForUser('${u.id}', '${u.username}')"
|
||||
title="${_('add_connection')}">${uiIcon('link')}</button>
|
||||
<button class="btn btn-secondary btn-sm btn-icon"
|
||||
onclick='openEditUser({"id": "${u.id}", "username": "${u.username}", "tg": "${u.telegramId || ""}", "email": "${u.email || ""}", "desc": "${u.description || ""}", "limit": "${u.traffic_limit ? (u.traffic_limit / Math.pow(1024, 3)).toFixed(2) : 0}", "strategy": "${u.traffic_reset_strategy || 'never'}", "expiration": "${u.expiration_date || ''}"})'
|
||||
onclick='openEditUser({"id": "${u.id}", "username": "${u.username}", "tg": "${u.telegramId || ""}", "email": "${u.email || ""}", "desc": "${u.description || ""}", "limit": "${u.traffic_limit ? (u.traffic_limit / Math.pow(1024, 3)).toFixed(2) : 0}", "strategy": "${u.traffic_reset_strategy || 'never'}", "expiration": "${u.expiration_date || ''}", "expire_after_first_use": ${u.expire_after_first_use ? 'true' : 'false'}, "expiration_days": ${u.expiration_days || 0}})'
|
||||
title="${_('edit')}">${uiIcon('pencil')}</button>
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="openShareModal(this.dataset)"
|
||||
data-id="${u.id}" data-username="${u.username}" data-token="${u.share_token || ''}"
|
||||
@@ -665,8 +687,15 @@
|
||||
description: document.getElementById('newDescription').value || null,
|
||||
traffic_limit: parseFloat(document.getElementById('newTrafficLimit').value || '0'),
|
||||
traffic_reset_strategy: document.getElementById('newTrafficResetStrategy').value,
|
||||
expiration_date: getDateTimeValue('newExpirationDate', 'newExpirationTime'),
|
||||
expire_after_first_use: document.getElementById('newExpireAfterFirstUse').checked,
|
||||
expiration_days: parseInt(document.getElementById('newExpirationDays').value || '0'),
|
||||
expiration_date: document.getElementById('newExpireAfterFirstUse').checked
|
||||
? null
|
||||
: getDateTimeValue('newExpirationDate', 'newExpirationTime'),
|
||||
};
|
||||
if (body.expire_after_first_use && (!body.expiration_days || body.expiration_days < 1)) {
|
||||
throw new Error(_('expire_days_required'));
|
||||
}
|
||||
const serverId = document.getElementById('newUserServer').value;
|
||||
if (serverId !== '') {
|
||||
body.server_id = parseInt(serverId);
|
||||
@@ -981,6 +1010,27 @@
|
||||
return new Date(parsed.getTime() - offsetMs).toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
function toggleExpireAfterFirstUse(prefix) {
|
||||
const checked = document.getElementById(prefix === 'new' ? 'newExpireAfterFirstUse' : 'editExpireAfterFirstUse').checked;
|
||||
const abs = document.getElementById(prefix === 'new' ? 'newExpireAbsoluteWrap' : 'editExpireAbsoluteWrap');
|
||||
const days = document.getElementById(prefix === 'new' ? 'newExpireDaysWrap' : 'editExpireDaysWrap');
|
||||
if (abs) abs.style.display = checked ? 'none' : 'grid';
|
||||
if (days) days.style.display = checked ? '' : 'none';
|
||||
}
|
||||
|
||||
function formatExpirationBadge(u) {
|
||||
if (u.expire_after_first_use && !u.expiration_date) {
|
||||
const days = u.expiration_days || 0;
|
||||
return `<span style="color:var(--text-muted)">⌛ ${_('expire_pending_label').replace('{}', String(days))}</span>`;
|
||||
}
|
||||
if (u.expiration_date) {
|
||||
const expired = new Date(u.expiration_date) < new Date();
|
||||
const suffix = u.expire_after_first_use ? ` (${_('expire_started_suffix')})` : '';
|
||||
return `<span style="color: ${expired ? 'var(--danger)' : 'var(--text-muted)'}">⌛ ${_('expiration_date_label')}: ${new Date(u.expiration_date).toLocaleString()}${suffix}</span>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function openEditUser(data) {
|
||||
document.getElementById('editUserId').value = data.id;
|
||||
document.getElementById('editUserTitle').textContent = data.username;
|
||||
@@ -989,7 +1039,17 @@
|
||||
document.getElementById('editUserDesc').value = data.desc || '';
|
||||
document.getElementById('editUserLimit').value = data.limit || '0';
|
||||
document.getElementById('editUserTrafficResetStrategy').value = data.strategy || 'never';
|
||||
const afterFirst = !!data.expire_after_first_use;
|
||||
document.getElementById('editExpireAfterFirstUse').checked = afterFirst;
|
||||
document.getElementById('editExpirationDays').value = String(data.expiration_days || 30);
|
||||
setDateTimeValue('editUserExpiration', 'editUserExpirationTime', data.expiration);
|
||||
toggleExpireAfterFirstUse('edit');
|
||||
const hint = document.getElementById('editExpireDaysHint');
|
||||
if (hint) {
|
||||
hint.textContent = (afterFirst && data.expiration)
|
||||
? _('expire_already_started_hint').replace('{}', new Date(data.expiration).toLocaleString())
|
||||
: _('expire_after_first_use_hint');
|
||||
}
|
||||
document.getElementById('editUserPass').value = '';
|
||||
openModal('editUserModal');
|
||||
}
|
||||
@@ -997,34 +1057,39 @@
|
||||
async function saveEditUser() {
|
||||
const uid = document.getElementById('editUserId').value;
|
||||
const btn = document.getElementById('saveEditBtn');
|
||||
const text = document.getElementById('saveEditBtnText');
|
||||
const spinner = document.getElementById('saveEditSpinner');
|
||||
|
||||
const payload = {
|
||||
telegramId: document.getElementById('editUserTG').value,
|
||||
email: document.getElementById('editUserEmail').value,
|
||||
description: document.getElementById('editUserDesc').value,
|
||||
const afterFirst = document.getElementById('editExpireAfterFirstUse').checked;
|
||||
const days = parseInt(document.getElementById('editExpirationDays').value || '0');
|
||||
if (afterFirst && days < 1) {
|
||||
showToast(_('expire_days_required'), 'error');
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
telegramId: document.getElementById('editUserTG').value || null,
|
||||
email: document.getElementById('editUserEmail').value || null,
|
||||
description: document.getElementById('editUserDesc').value || null,
|
||||
traffic_limit: parseFloat(document.getElementById('editUserLimit').value || '0'),
|
||||
traffic_reset_strategy: document.getElementById('editUserTrafficResetStrategy').value,
|
||||
expiration_date: getDateTimeValue('editUserExpiration', 'editUserExpirationTime'),
|
||||
password: document.getElementById('editUserPass').value || null
|
||||
expire_after_first_use: afterFirst,
|
||||
expiration_days: afterFirst ? days : 0,
|
||||
expiration_date: afterFirst ? null : getDateTimeValue('editUserExpiration', 'editUserExpirationTime'),
|
||||
};
|
||||
|
||||
// If countdown already started, keep absolute date unless admin cleared first-use mode
|
||||
if (afterFirst) {
|
||||
const existing = getDateTimeValue('editUserExpiration', 'editUserExpirationTime');
|
||||
if (existing) body.expiration_date = existing;
|
||||
}
|
||||
const pwd = document.getElementById('editUserPass').value;
|
||||
if (pwd) body.password = pwd;
|
||||
btn.disabled = true;
|
||||
text.textContent = _('saving');
|
||||
spinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
await apiCall(`/api/users/${uid}/update`, 'POST', payload);
|
||||
await apiCall(`/api/users/${uid}/update`, 'POST', body);
|
||||
showToast(_('success'), 'success');
|
||||
closeModal('editUserModal');
|
||||
refreshUsersList();
|
||||
setTimeout(() => window.location.reload(), 400);
|
||||
} catch (err) {
|
||||
showToast(`${_('error')}: ` + err.message, 'error');
|
||||
showToast(`${_('error')}: ${err.message}`, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
text.textContent = _('save');
|
||||
spinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user