add share-link password/expiry, journal touch-ups, ddns helper and audit docs

This commit is contained in:
dev
2026-09-08 10:54:24 +03:00
parent dd5a2ea288
commit 57f2ea4f41
11 changed files with 626 additions and 63 deletions
+29 -1
View File
@@ -41,6 +41,20 @@
<input type="date" id="linkDateTo">
</div>
</div>
<div>
<label>Истекает через (дни)</label>
<input type="number" id="linkExpiresDays" min="1" max="365" value="7" placeholder="7">
</div>
<div>
<label>
<input type="checkbox" id="linkAnonymizeNames">
Показывать только инициалы учеников (анонимизировать)
</label>
</div>
<div>
<label>Пароль для доступа (необязательно)</label>
<input type="password" id="linkPassword" placeholder="Оставьте пустым для открытого доступа">
</div>
<div class="actions">
<button class="cancel" onclick="closeLinkModal()">Отмена</button>
<button class="save" onclick="saveLink()">Сохранить</button>
@@ -71,6 +85,9 @@ async function loadLinks() {
${l.group_name ? `Группа: <b>${esc(l.group_name)}</b>` : ''}
${l.student_name ? `${l.group_name ? ' · ' : ''}Ученик: <b>${esc(l.student_name)}</b>` : ''}
${l.date_from || l.date_to ? ` · период: <b>${l.date_from ? l.date_from.split('-').reverse().join('.') : '…'}</b> — <b>${l.date_to ? l.date_to.split('-').reverse().join('.') : '…'}</b>` : ' · весь период'}
${l.expires_at ? ` · истекает ${new Date(l.expires_at).toLocaleDateString('ru')}` : ''}
${l.anonymize_names ? ` · <span style="color:#f59e0b">анонимизировано</span>` : ''}
${l.access_password_hash ? ` · <span style="color:#2563eb">🔒 пароль</span>` : ''}
· создана ${new Date(l.created_at).toLocaleString('ru')}
</div>
</div>
@@ -92,6 +109,9 @@ async function openLinkModal() {
document.getElementById('linkName').value = '';
document.getElementById('linkDateFrom').value = '';
document.getElementById('linkDateTo').value = '';
document.getElementById('linkExpiresDays').value = '7';
document.getElementById('linkAnonymizeNames').checked = false;
document.getElementById('linkPassword').value = '';
if (!groups.length) {
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
groups = await res.json();
@@ -116,6 +136,9 @@ async function openLinkEdit(id) {
document.getElementById('linkName').value = l.name;
document.getElementById('linkDateFrom').value = l.date_from || '';
document.getElementById('linkDateTo').value = l.date_to || '';
document.getElementById('linkExpiresDays').value = l.expires_at ? Math.ceil((new Date(l.expires_at) - new Date()) / (1000 * 60 * 60 * 24)) : '7';
document.getElementById('linkAnonymizeNames').checked = l.anonymize_names || false;
document.getElementById('linkPassword').value = ''; // Never show existing password
if (!groups.length) {
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
groups = await res.json();
@@ -145,8 +168,13 @@ async function saveLink() {
const student_name = document.getElementById('linkStudent').value;
const date_from = document.getElementById('linkDateFrom').value;
const date_to = document.getElementById('linkDateTo').value;
const expires_days = parseInt(document.getElementById('linkExpiresDays').value, 10);
const anonymize_names = document.getElementById('linkAnonymizeNames').checked;
const access_password = document.getElementById('linkPassword').value;
if (!name) { alert('Введите название'); return; }
const body = JSON.stringify({ name, group_id: group_id || null, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null });
if (expires_days < 1 || expires_days > 365) { alert('Дней истечения: 1-365'); return; }
const expires_at = new Date(Date.now() + expires_days * 24 * 60 * 60 * 1000).toISOString();
const body = JSON.stringify({ name, group_id: group_id || null, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null, anonymize_names, expires_at, access_password: access_password || null });
const res = await fetch(editingId ? `${API}/api/links/${editingId}` : `${API}/api/links`, {
method: editingId ? 'PUT' : 'POST',
headers: hdrJson(),