style(front/js): apply types on all objects
- reduce amount of warnings in IDE
- maximize API type mapping with static/js/core/types.js
This commit is contained in:
+169
-109
@@ -3,13 +3,20 @@ import { escapeHtml } from '../../core/formatters.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { oxiIconsInit } from '../../core/icons.js';
|
||||
|
||||
/**
|
||||
* @import {RoleEnum} from '../../core/types.js'
|
||||
*/
|
||||
|
||||
const API = '/api';
|
||||
let currentAdminId = '';
|
||||
let usersPage = 0;
|
||||
const PAGE_SIZE = 50;
|
||||
let totalUsers = 0;
|
||||
|
||||
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
||||
/**
|
||||
* Escape a string for safe embedding inside a JS string literal within an HTML attribute.
|
||||
* @param {string} s
|
||||
*/
|
||||
function _escJs(s) {
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.replace(/[^\w .-]/g, (c) => {
|
||||
@@ -17,6 +24,7 @@ function _escJs(s) {
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function hideElement(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
@@ -24,6 +32,10 @@ function hideElement(id) {
|
||||
element.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {string} [mode]
|
||||
*/
|
||||
function showElement(id, mode = 'block') {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
@@ -39,6 +51,7 @@ function headers() {
|
||||
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
||||
}
|
||||
|
||||
/** @param {number} bytes */
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024,
|
||||
@@ -47,11 +60,12 @@ function formatBytes(bytes) {
|
||||
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/** @param {string|null} dateStr */
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr) return i18n.t('admin.never');
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const secs = Math.floor((now - d) / 1000);
|
||||
const secs = Math.floor((now.getTime() - d.getTime()) / 1000);
|
||||
if (secs < 60) return i18n.t('admin.just_now');
|
||||
if (secs < 3600) return i18n.t('admin.minutes_ago', { n: Math.floor(secs / 60) });
|
||||
if (secs < 86400) return i18n.t('admin.hours_ago', { n: Math.floor(secs / 3600) });
|
||||
@@ -60,6 +74,7 @@ function timeAgo(dateStr) {
|
||||
}
|
||||
|
||||
/* ── Custom confirm modal ── */
|
||||
/** @param {string} message */
|
||||
function showConfirm(message) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.getElementById('confirm-modal');
|
||||
@@ -70,6 +85,7 @@ function showConfirm(message) {
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.classList.add('show-flex');
|
||||
|
||||
/** @param {any} result */
|
||||
function cleanup(result) {
|
||||
overlay.classList.remove('show-flex');
|
||||
overlay.classList.add('hidden');
|
||||
@@ -84,6 +100,7 @@ function showConfirm(message) {
|
||||
function onNo() {
|
||||
cleanup(false);
|
||||
}
|
||||
/** @param {Event} e */
|
||||
function onOverlay(e) {
|
||||
if (e.target === overlay) cleanup(false);
|
||||
}
|
||||
@@ -96,6 +113,10 @@ function showConfirm(message) {
|
||||
/* ── Tab switching with fade animation ── */
|
||||
let activeTabName = 'dashboard';
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Element|undefined} el
|
||||
*/
|
||||
function switchTab(name, el) {
|
||||
if (name === activeTabName) return;
|
||||
var oldTab = document.getElementById(`tab-${activeTabName}`);
|
||||
@@ -158,7 +179,7 @@ async function loadDashboard() {
|
||||
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
|
||||
|
||||
if (typeof d.registration_enabled !== 'undefined') {
|
||||
document.getElementById('ds-registration').checked = d.registration_enabled;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('ds-registration')).checked = d.registration_enabled;
|
||||
if (d.registration_enabled) hideElement('registration-warning');
|
||||
else showElement('registration-warning', 'flex');
|
||||
}
|
||||
@@ -200,7 +221,7 @@ async function loadUsers() {
|
||||
}
|
||||
|
||||
tbody.innerHTML = users
|
||||
.map((u) => {
|
||||
.map((/** @type {any} */ u) => {
|
||||
const quotaPct = u.storage_quota_bytes > 0 ? (u.storage_used_bytes / u.storage_quota_bytes) * 100 : 0;
|
||||
const quotaColor = quotaPct > 90 ? 'red' : quotaPct > 70 ? 'orange' : 'green';
|
||||
const quotaText =
|
||||
@@ -306,18 +327,18 @@ async function loadUsers() {
|
||||
.join('');
|
||||
|
||||
// Set dynamic progress bar widths (CSP-safe via JS property)
|
||||
document.querySelectorAll('.progress-fill[data-width]').forEach((el) => {
|
||||
/** @type {NodeListOf<HTMLDivElement>} */ (document.querySelectorAll('.progress-fill[data-width]')).forEach((el) => {
|
||||
el.style.width = `${el.dataset.width}%`;
|
||||
el.removeAttribute('data-width');
|
||||
});
|
||||
|
||||
// Wire up admin action buttons (replaces inline onclick handlers)
|
||||
document.querySelectorAll('.admin-action-btn').forEach((btn) => {
|
||||
/** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.admin-action-btn')).forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'quota') openQuotaModal(btn.dataset.uid, btn.dataset.uname, Number(btn.dataset.quota));
|
||||
else if (action === 'reset-pw') openResetPasswordModal(btn.dataset.uid, btn.dataset.uname);
|
||||
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, btn.dataset.role);
|
||||
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, /** @type {RoleEnum} */ (btn.dataset.role));
|
||||
else if (action === 'toggle-active') toggleActive(btn.dataset.uid, btn.dataset.active === 'true');
|
||||
else if (action === 'delete') deleteUser(btn.dataset.uid, btn.dataset.uname);
|
||||
});
|
||||
@@ -326,12 +347,12 @@ async function loadUsers() {
|
||||
const from = usersPage * PAGE_SIZE + 1;
|
||||
const to = Math.min((usersPage + 1) * PAGE_SIZE, totalUsers);
|
||||
document.getElementById('users-info').textContent = i18n.t('admin.showing_users', { from: from, to: to, total: totalUsers });
|
||||
document.getElementById('prev-btn').disabled = usersPage === 0;
|
||||
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
|
||||
/** @type {HTMLButtonElement} */ (document.getElementById('prev-btn')).disabled = usersPage === 0;
|
||||
/** @type {HTMLButtonElement} */ (document.getElementById('next-btn')).disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
|
||||
} catch (e) {
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||
escapeHtml(i18n.t('admin.error_network', { message: e.message })) +
|
||||
escapeHtml(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message })) +
|
||||
'</td></tr>';
|
||||
}
|
||||
}
|
||||
@@ -349,6 +370,10 @@ function nextPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {RoleEnum} currentRole
|
||||
*/
|
||||
async function toggleRole(userId, currentRole) {
|
||||
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
||||
const ok = await showConfirm(i18n.t('admin.confirm_role_change', { role: newRole }));
|
||||
@@ -366,10 +391,14 @@ async function toggleRole(userId, currentRole) {
|
||||
alert(e.message || i18n.t('admin.error_generic'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||
alert(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {boolean} currentActive
|
||||
*/
|
||||
async function toggleActive(userId, currentActive) {
|
||||
const msg = currentActive ? i18n.t('admin.confirm_deactivate') : i18n.t('admin.confirm_activate');
|
||||
const ok = await showConfirm(msg);
|
||||
@@ -387,10 +416,14 @@ async function toggleActive(userId, currentActive) {
|
||||
alert(e.message || i18n.t('admin.error_generic'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||
alert(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {string} username
|
||||
*/
|
||||
async function deleteUser(userId, username) {
|
||||
const ok = await showConfirm(i18n.t('admin.confirm_delete_user', { name: username }));
|
||||
if (!ok) return;
|
||||
@@ -408,17 +441,22 @@ async function deleteUser(userId, username) {
|
||||
alert(e.message || i18n.t('admin.error_generic'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||
alert(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }));
|
||||
}
|
||||
}
|
||||
|
||||
let quotaUserId = '';
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {string} username
|
||||
* @param {number} currentQuota
|
||||
*/
|
||||
function openQuotaModal(userId, username, currentQuota) {
|
||||
quotaUserId = userId;
|
||||
document.getElementById('qm-username').textContent = username;
|
||||
const gb = currentQuota / 1073741824;
|
||||
document.getElementById('qm-unit').value = '1073741824';
|
||||
document.getElementById('qm-value').value = gb > 0 ? Math.round(gb * 10) / 10 : 0;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('qm-unit')).value = '1073741824';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('qm-value')).value = String(gb > 0 ? Math.round(gb * 10) / 10 : 0);
|
||||
showElement('quota-modal', 'flex');
|
||||
}
|
||||
function closeQuotaModal() {
|
||||
@@ -426,8 +464,8 @@ function closeQuotaModal() {
|
||||
}
|
||||
|
||||
async function saveQuota() {
|
||||
const val = parseFloat(document.getElementById('qm-value').value) || 0;
|
||||
const unit = parseInt(document.getElementById('qm-unit').value, 10);
|
||||
const val = parseFloat(/** @type {HTMLInputElement} */ (document.getElementById('qm-value')).value) || 0;
|
||||
const unit = parseInt(/** @type {HTMLInputElement} */ (document.getElementById('qm-unit')).value, 10);
|
||||
const bytes = Math.round(val * unit);
|
||||
try {
|
||||
const resp = await fetch(`${API}/admin/users/${quotaUserId}/quota`, {
|
||||
@@ -445,33 +483,33 @@ async function saveQuota() {
|
||||
alert(e.message || i18n.t('admin.error_generic'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||
alert(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }));
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateUserModal() {
|
||||
document.getElementById('cu-username').value = '';
|
||||
document.getElementById('cu-password').value = '';
|
||||
document.getElementById('cu-email').value = '';
|
||||
document.getElementById('cu-role').value = 'user';
|
||||
document.getElementById('cu-quota-value').value = '1';
|
||||
document.getElementById('cu-quota-unit').value = '1073741824';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('cu-username')).value = '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('cu-password')).value = '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('cu-email')).value = '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('cu-role')).value = 'user';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('cu-quota-value')).value = '1';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('cu-quota-unit')).value = '1073741824';
|
||||
document.getElementById('cu-error').className = 'alert';
|
||||
document.getElementById('cu-error').textContent = '';
|
||||
showElement('create-user-modal', 'flex');
|
||||
setTimeout(() => document.getElementById('cu-username').focus(), 100);
|
||||
setTimeout(() => /** @type {HTMLInputElement} */ (document.getElementById('cu-username')).focus(), 100);
|
||||
}
|
||||
function closeCreateUserModal() {
|
||||
hideElement('create-user-modal');
|
||||
}
|
||||
|
||||
async function submitCreateUser() {
|
||||
const username = document.getElementById('cu-username').value.trim();
|
||||
const password = document.getElementById('cu-password').value;
|
||||
const email = document.getElementById('cu-email').value.trim() || null;
|
||||
const role = document.getElementById('cu-role').value;
|
||||
const quotaVal = parseFloat(document.getElementById('cu-quota-value').value) || 0;
|
||||
const quotaUnit = parseInt(document.getElementById('cu-quota-unit').value, 10);
|
||||
const username = /** @type {HTMLInputElement} */ (document.getElementById('cu-username')).value.trim();
|
||||
const password = /** @type {HTMLInputElement} */ (document.getElementById('cu-password')).value;
|
||||
const email = /** @type {HTMLInputElement} */ (document.getElementById('cu-email')).value.trim() || null;
|
||||
const role = /** @type {HTMLInputElement} */ (document.getElementById('cu-role')).value;
|
||||
const quotaVal = parseFloat(/** @type {HTMLInputElement} */ (document.getElementById('cu-quota-value')).value) || 0;
|
||||
const quotaUnit = parseInt(/** @type {HTMLInputElement} */ (document.getElementById('cu-quota-unit')).value, 10);
|
||||
const quotaBytes = Math.round(quotaVal * quotaUnit);
|
||||
|
||||
const errorEl = document.getElementById('cu-error');
|
||||
@@ -486,7 +524,7 @@ async function submitCreateUser() {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('cu-submit');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('cu-submit'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.creating'))}`;
|
||||
try {
|
||||
@@ -512,7 +550,7 @@ async function submitCreateUser() {
|
||||
errorEl.className = 'alert alert-error';
|
||||
}
|
||||
} catch (e) {
|
||||
errorEl.textContent = i18n.t('admin.error_network', { message: e.message });
|
||||
errorEl.textContent = i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message });
|
||||
errorEl.className = 'alert alert-error';
|
||||
}
|
||||
btn.disabled = false;
|
||||
@@ -520,21 +558,25 @@ async function submitCreateUser() {
|
||||
}
|
||||
|
||||
let resetPwUserId = '';
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {string} username
|
||||
*/
|
||||
function openResetPasswordModal(userId, username) {
|
||||
resetPwUserId = userId;
|
||||
document.getElementById('rp-username').textContent = username;
|
||||
document.getElementById('rp-password').value = '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('rp-password')).value = '';
|
||||
document.getElementById('rp-error').className = 'alert';
|
||||
document.getElementById('rp-error').textContent = '';
|
||||
showElement('reset-pw-modal', 'flex');
|
||||
setTimeout(() => document.getElementById('rp-password').focus(), 100);
|
||||
setTimeout(() => /** @type {HTMLInputElement} */ (document.getElementById('rp-password')).focus(), 100);
|
||||
}
|
||||
function closeResetPasswordModal() {
|
||||
hideElement('reset-pw-modal');
|
||||
}
|
||||
|
||||
async function submitResetPassword() {
|
||||
const password = document.getElementById('rp-password').value;
|
||||
const password = /** @type {HTMLInputElement} */ (document.getElementById('rp-password')).value;
|
||||
const errorEl = document.getElementById('rp-error');
|
||||
if (password.length < 8) {
|
||||
errorEl.textContent = i18n.t('admin.error_password_short');
|
||||
@@ -542,7 +584,7 @@ async function submitResetPassword() {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('rp-submit');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('rp-submit'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.resetting'))}`;
|
||||
try {
|
||||
@@ -560,13 +602,14 @@ async function submitResetPassword() {
|
||||
errorEl.className = 'alert alert-error';
|
||||
}
|
||||
} catch (e) {
|
||||
errorEl.textContent = i18n.t('admin.error_network', { message: e.message });
|
||||
errorEl.textContent = i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message });
|
||||
errorEl.className = 'alert alert-error';
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.reset_btn'))}`;
|
||||
}
|
||||
|
||||
/** @param {boolean} enabled */
|
||||
async function toggleRegistration(enabled) {
|
||||
if (enabled) hideElement('registration-warning');
|
||||
else showElement('registration-warning', 'flex');
|
||||
@@ -578,29 +621,33 @@ async function toggleRegistration(enabled) {
|
||||
body: JSON.stringify({ registration_enabled: enabled })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
document.getElementById('ds-registration').checked = !enabled;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('ds-registration')).checked = !enabled;
|
||||
if (!enabled) showElement('registration-warning', 'flex');
|
||||
else hideElement('registration-warning');
|
||||
const e = await resp.json().catch(() => ({}));
|
||||
alert(e.message || i18n.t('admin.error_generic'));
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('ds-registration').checked = !enabled;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('ds-registration')).checked = !enabled;
|
||||
if (!enabled) showElement('registration-warning', 'flex');
|
||||
else hideElement('registration-warning');
|
||||
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||
alert(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }));
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('oidc-enabled').addEventListener('change', function () {
|
||||
if (this.checked) showElement('oidc-form');
|
||||
if (/** @type {HTMLInputElement} */ (this).checked) showElement('oidc-form');
|
||||
else hideElement('oidc-form');
|
||||
});
|
||||
document.getElementById('disable-password').addEventListener('change', function () {
|
||||
if (this.checked) showElement('password-warning', 'flex');
|
||||
if (/** @type {HTMLInputElement} */ (this).checked) showElement('password-warning', 'flex');
|
||||
else hideElement('password-warning');
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
* @param {string} type
|
||||
*/
|
||||
function showOidcStatus(msg, type) {
|
||||
const el = document.getElementById('oidc-status');
|
||||
el.textContent = msg;
|
||||
@@ -613,12 +660,12 @@ function copyCallback() {
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
const url = document.getElementById('issuer-url').value.trim();
|
||||
const url = /** @type {HTMLInputElement} */ (document.getElementById('issuer-url')).value.trim();
|
||||
if (!url) {
|
||||
showOidcStatus('Enter an Issuer URL first', 'error');
|
||||
return;
|
||||
}
|
||||
const btn = document.getElementById('discover-btn');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('discover-btn'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.discovering'))}`;
|
||||
const resultDiv = document.getElementById('discovery-result');
|
||||
@@ -639,32 +686,32 @@ async function testConnection() {
|
||||
'</dd><dt>Auth Endpoint</dt><dd>' +
|
||||
escapeHtml(r.authorization_endpoint || '—') +
|
||||
'</dd></dl></div>';
|
||||
if (!document.getElementById('provider-name').value && r.provider_name_suggestion)
|
||||
document.getElementById('provider-name').value = r.provider_name_suggestion;
|
||||
if (!(/** @type {HTMLInputElement} */ (document.getElementById('provider-name')).value) && r.provider_name_suggestion)
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('provider-name')).value = r.provider_name_suggestion;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(r.message)}</strong></div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
||||
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(/** @type {Error} */ (e).message)}</div>`;
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(i18n.t('admin.auto_discover'))}`;
|
||||
}
|
||||
|
||||
async function saveOidcSettings() {
|
||||
const btn = document.getElementById('save-btn');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('save-btn'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.saving'))}`;
|
||||
const body = {
|
||||
enabled: document.getElementById('oidc-enabled').checked,
|
||||
issuer_url: document.getElementById('issuer-url').value.trim(),
|
||||
client_id: document.getElementById('client-id').value.trim(),
|
||||
client_secret: document.getElementById('client-secret').value || null,
|
||||
scopes: document.getElementById('scopes').value.trim() || null,
|
||||
auto_provision: document.getElementById('auto-provision').checked,
|
||||
admin_groups: document.getElementById('admin-groups').value.trim() || null,
|
||||
disable_password_login: document.getElementById('disable-password').checked,
|
||||
provider_name: document.getElementById('provider-name').value.trim() || null
|
||||
enabled: /** @type {HTMLInputElement} */ (document.getElementById('oidc-enabled')).checked,
|
||||
issuer_url: /** @type {HTMLInputElement} */ (document.getElementById('issuer-url')).value.trim(),
|
||||
client_id: /** @type {HTMLInputElement} */ (document.getElementById('client-id')).value.trim(),
|
||||
client_secret: /** @type {HTMLInputElement} */ (document.getElementById('client-secret')).value || null,
|
||||
scopes: /** @type {HTMLInputElement} */ (document.getElementById('scopes')).value.trim() || null,
|
||||
auto_provision: /** @type {HTMLInputElement} */ (document.getElementById('auto-provision')).checked,
|
||||
admin_groups: /** @type {HTMLInputElement} */ (document.getElementById('admin-groups')).value.trim() || null,
|
||||
disable_password_login: /** @type {HTMLInputElement} */ (document.getElementById('disable-password')).checked,
|
||||
provider_name: /** @type {HTMLInputElement} */ (document.getElementById('provider-name')).value.trim() || null
|
||||
};
|
||||
try {
|
||||
const resp = await fetch(`${API}/admin/settings/oidc`, {
|
||||
@@ -682,7 +729,7 @@ async function saveOidcSettings() {
|
||||
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showOidcStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||
showOidcStatus(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }), 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.save_btn'))}`;
|
||||
@@ -701,20 +748,25 @@ const STORAGE_PRESETS = {
|
||||
'wasabi': { endpoint: 'https://s3.{region}.wasabisys.com', region: 'us-east-1', pathStyle: false },
|
||||
};
|
||||
|
||||
/** @param {boolean} visible */
|
||||
function toggleS3Form(visible) {
|
||||
if (visible) showElement('storage-s3-form');
|
||||
else hideElement('storage-s3-form');
|
||||
}
|
||||
|
||||
function onStoragePresetChange() {
|
||||
const preset = document.getElementById('storage-preset').value;
|
||||
const p = STORAGE_PRESETS[preset];
|
||||
const preset = /** @type {HTMLInputElement} */ (document.getElementById('storage-preset')).value;
|
||||
const p = STORAGE_PRESETS[/** @type {keyof typeof STORAGE_PRESETS} */ (preset)];
|
||||
if (!p) return;
|
||||
if (p.endpoint) document.getElementById('storage-endpoint-url').value = p.endpoint;
|
||||
if (p.region) document.getElementById('storage-region').value = p.region;
|
||||
document.getElementById('storage-path-style').checked = p.pathStyle;
|
||||
if (p.endpoint) /** @type {HTMLInputElement} */ (document.getElementById('storage-endpoint-url')).value = p.endpoint;
|
||||
if (p.region) /** @type {HTMLInputElement} */ (document.getElementById('storage-region')).value = p.region;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-path-style')).checked = p.pathStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
* @param {string} type
|
||||
*/
|
||||
function showStorageStatus(msg, type) {
|
||||
const el = document.getElementById('storage-status');
|
||||
el.textContent = msg;
|
||||
@@ -732,21 +784,23 @@ async function loadStorage() {
|
||||
|
||||
// Backend selector
|
||||
document.querySelectorAll('input[name="storage-backend"]').forEach((r) => {
|
||||
r.checked = r.value === s.backend;
|
||||
const input = /** @type {HTMLInputElement} */ (r);
|
||||
input.checked = input.value === s.backend;
|
||||
});
|
||||
toggleS3Form(s.backend === 's3');
|
||||
|
||||
// S3 fields
|
||||
document.getElementById('storage-endpoint-url').value = s.s3_endpoint_url || '';
|
||||
document.getElementById('storage-bucket').value = s.s3_bucket || '';
|
||||
document.getElementById('storage-region').value = s.s3_region || '';
|
||||
document.getElementById('storage-access-key').value = '';
|
||||
document.getElementById('storage-secret-key').value = '';
|
||||
document.getElementById('storage-path-style').checked = s.s3_force_path_style;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-endpoint-url')).value = s.s3_endpoint_url || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-bucket')).value = s.s3_bucket || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-region')).value = s.s3_region || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-access-key')).value = '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-secret-key')).value = '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-path-style')).checked = s.s3_force_path_style;
|
||||
|
||||
// Secret hints
|
||||
if (s.s3_access_key_set) {
|
||||
document.getElementById('storage-access-key').placeholder = i18n.t('admin.storage_key_placeholder') || 'Leave empty to keep current value';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('storage-access-key')).placeholder =
|
||||
i18n.t('admin.storage_key_placeholder') || 'Leave empty to keep current value';
|
||||
}
|
||||
if (s.s3_secret_key_set) {
|
||||
showElement('storage-secret-hint');
|
||||
@@ -755,7 +809,7 @@ async function loadStorage() {
|
||||
}
|
||||
|
||||
// ENV badges
|
||||
(s.env_overrides || []).forEach((field) => {
|
||||
/** @type {string[]} */ (s.env_overrides || []).forEach((field) => {
|
||||
const badge = document.getElementById(`badge-${field}`);
|
||||
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
|
||||
});
|
||||
@@ -766,7 +820,7 @@ async function loadStorage() {
|
||||
document.getElementById('storage-total-size').textContent = s.total_bytes_stored != null ? formatBytes(s.total_bytes_stored) : '—';
|
||||
document.getElementById('storage-dedup-ratio').textContent = s.dedup_ratio != null ? `${s.dedup_ratio.toFixed(2)}x` : '—';
|
||||
} catch (e) {
|
||||
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||
showStorageStatus(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }), 'error');
|
||||
}
|
||||
|
||||
// Also load migration status
|
||||
@@ -774,19 +828,19 @@ async function loadStorage() {
|
||||
}
|
||||
|
||||
async function saveStorageSettings() {
|
||||
const btn = document.getElementById('btn-save-storage');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('btn-save-storage'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.saving'))}`;
|
||||
|
||||
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
|
||||
const backend = /** @type {HTMLInputElement} */ (document.querySelector('input[name="storage-backend"]:checked')).value;
|
||||
const body = {
|
||||
backend,
|
||||
s3_endpoint_url: document.getElementById('storage-endpoint-url').value.trim() || null,
|
||||
s3_bucket: document.getElementById('storage-bucket').value.trim() || null,
|
||||
s3_region: document.getElementById('storage-region').value.trim() || null,
|
||||
s3_access_key: document.getElementById('storage-access-key').value || null,
|
||||
s3_secret_key: document.getElementById('storage-secret-key').value || null,
|
||||
s3_force_path_style: document.getElementById('storage-path-style').checked
|
||||
s3_endpoint_url: /** @type {HTMLInputElement} */ (document.getElementById('storage-endpoint-url')).value.trim() || null,
|
||||
s3_bucket: /** @type {HTMLInputElement} */ (document.getElementById('storage-bucket')).value.trim() || null,
|
||||
s3_region: /** @type {HTMLInputElement} */ (document.getElementById('storage-region')).value.trim() || null,
|
||||
s3_access_key: /** @type {HTMLInputElement} */ (document.getElementById('storage-access-key')).value || null,
|
||||
s3_secret_key: /** @type {HTMLInputElement} */ (document.getElementById('storage-secret-key')).value || null,
|
||||
s3_force_path_style: /** @type {HTMLInputElement} */ (document.getElementById('storage-path-style')).checked
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -804,26 +858,26 @@ async function saveStorageSettings() {
|
||||
showStorageStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||
showStorageStatus(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }), 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.storage_save') || 'Save')}`;
|
||||
}
|
||||
|
||||
async function testStorageConnection() {
|
||||
const btn = document.getElementById('btn-test-storage');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('btn-test-storage'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.testing') || 'Testing...')}`;
|
||||
|
||||
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
|
||||
const backend = /** @type {HTMLInputElement} */ (document.querySelector('input[name="storage-backend"]:checked')).value;
|
||||
const body = {
|
||||
backend,
|
||||
s3_endpoint_url: document.getElementById('storage-endpoint-url').value.trim() || null,
|
||||
s3_bucket: document.getElementById('storage-bucket').value.trim() || null,
|
||||
s3_region: document.getElementById('storage-region').value.trim() || null,
|
||||
s3_access_key: document.getElementById('storage-access-key').value || null,
|
||||
s3_secret_key: document.getElementById('storage-secret-key').value || null,
|
||||
s3_force_path_style: document.getElementById('storage-path-style').checked
|
||||
s3_endpoint_url: /** @type {HTMLInputElement} */ (document.getElementById('storage-endpoint-url')).value.trim() || null,
|
||||
s3_bucket: /** @type {HTMLInputElement} */ (document.getElementById('storage-bucket')).value.trim() || null,
|
||||
s3_region: /** @type {HTMLInputElement} */ (document.getElementById('storage-region')).value.trim() || null,
|
||||
s3_access_key: /** @type {HTMLInputElement} */ (document.getElementById('storage-access-key')).value || null,
|
||||
s3_secret_key: /** @type {HTMLInputElement} */ (document.getElementById('storage-secret-key')).value || null,
|
||||
s3_force_path_style: /** @type {HTMLInputElement} */ (document.getElementById('storage-path-style')).checked
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -842,7 +896,7 @@ async function testStorageConnection() {
|
||||
showStorageStatus(`${i18n.t('admin.storage_test_failure') || 'Connection failed'}: ${escapeHtml(r.message)}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||
showStorageStatus(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }), 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-vial"></i> ${escapeHtml(i18n.t('admin.storage_test_connection') || 'Test Connection')}`;
|
||||
@@ -850,8 +904,13 @@ async function testStorageConnection() {
|
||||
|
||||
/* ── Migration ── */
|
||||
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
let migrationPollTimer = null;
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
* @param {string} type
|
||||
*/
|
||||
function showMigrationMsg(msg, type) {
|
||||
const el = document.getElementById('migration-status-msg');
|
||||
el.textContent = msg;
|
||||
@@ -859,6 +918,7 @@ function showMigrationMsg(msg, type) {
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
/** @param {any} m */
|
||||
function updateMigrationUI(m) {
|
||||
// Status badge
|
||||
const badge = document.getElementById('migration-status-badge');
|
||||
@@ -937,7 +997,7 @@ async function loadMigrationStatus() {
|
||||
}
|
||||
|
||||
async function startMigration() {
|
||||
const btn = document.getElementById('btn-start-migration');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('btn-start-migration'));
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`${API}/admin/storage/migration/start`, {
|
||||
@@ -954,7 +1014,7 @@ async function startMigration() {
|
||||
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||
showMigrationMsg(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }), 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
@@ -992,7 +1052,7 @@ async function resumeMigration() {
|
||||
}
|
||||
|
||||
async function verifyMigration() {
|
||||
const btn = document.getElementById('btn-verify-migration');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('btn-verify-migration'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.migration_verifying') || 'Verifying...')}`;
|
||||
const resultDiv = document.getElementById('migration-verify-result');
|
||||
@@ -1015,7 +1075,7 @@ async function verifyMigration() {
|
||||
}
|
||||
} catch (e) {
|
||||
resultDiv.style.display = '';
|
||||
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
||||
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(/** @type {Error} */ (e).message)}</div>`;
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-check-double"></i> ${escapeHtml(i18n.t('admin.migration_verify') || 'Verify Integrity')}`;
|
||||
@@ -1036,7 +1096,7 @@ async function completeMigration() {
|
||||
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||
showMigrationMsg(i18n.t('admin.error_network', { message: /** @type {Error} */ (e).message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,21 +1124,21 @@ async function init() {
|
||||
});
|
||||
if (oidcResp.ok) {
|
||||
const s = await oidcResp.json();
|
||||
document.getElementById('oidc-enabled').checked = s.enabled;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('oidc-enabled')).checked = s.enabled;
|
||||
if (s.enabled) showElement('oidc-form');
|
||||
else hideElement('oidc-form');
|
||||
document.getElementById('provider-name').value = s.provider_name || '';
|
||||
document.getElementById('issuer-url').value = s.issuer_url || '';
|
||||
document.getElementById('client-id').value = s.client_id || '';
|
||||
document.getElementById('scopes').value = s.scopes || 'openid profile email';
|
||||
document.getElementById('auto-provision').checked = s.auto_provision;
|
||||
document.getElementById('admin-groups').value = s.admin_groups || '';
|
||||
document.getElementById('disable-password').checked = s.disable_password_login;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('provider-name')).value = s.provider_name || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('issuer-url')).value = s.issuer_url || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('client-id')).value = s.client_id || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('scopes')).value = s.scopes || 'openid profile email';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('auto-provision')).checked = s.auto_provision;
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('admin-groups')).value = s.admin_groups || '';
|
||||
/** @type {HTMLInputElement} */ (document.getElementById('disable-password')).checked = s.disable_password_login;
|
||||
if (s.disable_password_login) showElement('password-warning', 'flex');
|
||||
else hideElement('password-warning');
|
||||
document.getElementById('callback-url').textContent = s.callback_url;
|
||||
if (s.client_secret_set) showElement('secret-hint');
|
||||
(s.env_overrides || []).forEach((field) => {
|
||||
/** @type {string[]} */ (s.env_overrides || []).forEach((field) => {
|
||||
const badge = document.getElementById(`badge-${field}`);
|
||||
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
|
||||
});
|
||||
@@ -1128,7 +1188,7 @@ document.getElementById('tab-btn-storage').addEventListener('click', function ()
|
||||
});
|
||||
|
||||
document.getElementById('ds-registration').addEventListener('change', function () {
|
||||
toggleRegistration(this.checked);
|
||||
toggleRegistration(/** @type {HTMLInputElement} */ (this).checked);
|
||||
});
|
||||
|
||||
document.getElementById('btn-create-user').addEventListener('click', openCreateUserModal);
|
||||
@@ -1151,8 +1211,8 @@ document.getElementById('rp-submit').addEventListener('click', submitResetPasswo
|
||||
|
||||
/* ── Storage event listeners ── */
|
||||
document.querySelectorAll('input[name="storage-backend"]').forEach((r) => {
|
||||
r.addEventListener('change', function () {
|
||||
toggleS3Form(this.value === 's3');
|
||||
r.addEventListener('change', () => {
|
||||
toggleS3Form(/** @type {HTMLInputElement} */ (r).value === 's3');
|
||||
});
|
||||
});
|
||||
document.getElementById('storage-preset').addEventListener('change', onStoragePresetChange);
|
||||
|
||||
@@ -8,8 +8,10 @@ import { oxiIconsInit } from '../../core/icons.js';
|
||||
var deviceInfo = document.getElementById('device-info');
|
||||
var actionButtons = document.getElementById('action-buttons');
|
||||
var errorText = document.getElementById('error-text');
|
||||
var btnApprove = document.getElementById('btn-approve');
|
||||
var btnDeny = document.getElementById('btn-deny');
|
||||
var btnApprove = /** @type {HTMLButtonElement} */ (document.getElementById('btn-approve'));
|
||||
var btnDeny = /** @type {HTMLButtonElement} */ (document.getElementById('btn-deny'));
|
||||
|
||||
/** @type {ReturnType<typeof setTimeout>} */
|
||||
var debounceTimer = null;
|
||||
var currentCode = '';
|
||||
|
||||
@@ -24,12 +26,13 @@ import { oxiIconsInit } from '../../core/icons.js';
|
||||
|
||||
// Auto-insert hyphen and lookup on input
|
||||
codeInput.addEventListener('input', (e) => {
|
||||
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
|
||||
const target = /** @type {HTMLInputElement} */ (e.target);
|
||||
var val = target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
|
||||
// Auto-insert hyphen after 4 chars
|
||||
if (val.length === 4 && val.indexOf('-') === -1) {
|
||||
val = `${val}-`;
|
||||
}
|
||||
e.target.value = val;
|
||||
target.value = val;
|
||||
errorText.classList.add('hidden');
|
||||
|
||||
// Debounce lookup
|
||||
@@ -52,6 +55,11 @@ import { oxiIconsInit } from '../../core/icons.js';
|
||||
handleAction('deny');
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} code
|
||||
* @returns
|
||||
*/
|
||||
async function lookupCode(code) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/auth/device/verify?code=${encodeURIComponent(code)}`, {
|
||||
@@ -81,6 +89,10 @@ import { oxiIconsInit } from '../../core/icons.js';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {'approve' | 'deny'} action
|
||||
*/
|
||||
async function handleAction(action) {
|
||||
btnApprove.disabled = true;
|
||||
btnDeny.disabled = true;
|
||||
@@ -109,10 +121,14 @@ import { oxiIconsInit } from '../../core/icons.js';
|
||||
} catch (err) {
|
||||
btnApprove.disabled = false;
|
||||
btnDeny.disabled = false;
|
||||
showError(err.message || 'Failed to process action.');
|
||||
showError(/** @type {Error} */ (err).message || 'Failed to process action.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} msg
|
||||
*/
|
||||
function showError(msg) {
|
||||
errorText.textContent = msg;
|
||||
errorText.classList.remove('hidden');
|
||||
|
||||
@@ -6,7 +6,7 @@ if (!/^[0-9a-fA-F]+$/.test(token)) {
|
||||
document.body.innerHTML = '<p>Invalid session token.</p>';
|
||||
throw new Error('Invalid token format');
|
||||
}
|
||||
document.getElementById('login-flow-form').action = `/login/v2/flow/${token}`;
|
||||
/** @type {HTMLFormElement} */ (document.getElementById('login-flow-form')).action = `/login/v2/flow/${token}`;
|
||||
|
||||
// Check if OIDC is available and configure SSO button
|
||||
(async () => {
|
||||
|
||||
@@ -4,10 +4,16 @@ import { oxiIconsInit } from '../../core/icons.js';
|
||||
|
||||
const API = '/api';
|
||||
|
||||
// TOOD: reuse common library
|
||||
/**
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
function headers() {
|
||||
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
||||
}
|
||||
|
||||
// TOOD: move to common library
|
||||
/** @param {number} bytes */
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024,
|
||||
@@ -16,11 +22,13 @@ function formatBytes(bytes) {
|
||||
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
// TOOD: move to common library
|
||||
/** @param {string | null | undefined} dateStr */
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr) return i18n.t('profile.never');
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const secs = Math.floor((now - d) / 1000);
|
||||
const now = Date.now();
|
||||
const secs = Math.floor((now - d.valueOf()) / 1000);
|
||||
if (secs < 60) return i18n.t('profile.just_now');
|
||||
if (secs < 3600) return i18n.t('profile.minutes_ago', { n: Math.floor(secs / 60) });
|
||||
if (secs < 86400) return i18n.t('profile.hours_ago', { n: Math.floor(secs / 3600) });
|
||||
@@ -104,11 +112,12 @@ function showError() {
|
||||
document.getElementById('auth-error').classList.remove('hidden');
|
||||
}
|
||||
|
||||
/** @param {Event} e */
|
||||
async function changePassword(e) {
|
||||
e.preventDefault();
|
||||
const currentPw = document.getElementById('current-password').value;
|
||||
const newPw = document.getElementById('new-password').value;
|
||||
const confirmPw = document.getElementById('confirm-password').value;
|
||||
const currentPw = /** @type {HTMLInputElement} */ (document.getElementById('current-password')).value;
|
||||
const newPw = /** @type {HTMLInputElement} */ (document.getElementById('new-password')).value;
|
||||
const confirmPw = /** @type {HTMLInputElement} */ (document.getElementById('confirm-password')).value;
|
||||
const statusEl = document.getElementById('pw-status');
|
||||
|
||||
if (newPw !== confirmPw) {
|
||||
@@ -121,7 +130,7 @@ async function changePassword(e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('pw-submit');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('pw-submit'));
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('profile.updating'))}`;
|
||||
|
||||
@@ -138,7 +147,7 @@ async function changePassword(e) {
|
||||
|
||||
if (resp.ok) {
|
||||
statusEl.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${escapeHtml(i18n.t('profile.password_updated'))}</div>`;
|
||||
document.getElementById('password-form').reset();
|
||||
/** @type {HTMLFormElement} */ (document.getElementById('password-form')).reset();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
statusEl.innerHTML =
|
||||
@@ -149,7 +158,7 @@ async function changePassword(e) {
|
||||
} catch (err) {
|
||||
statusEl.innerHTML =
|
||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||
escapeHtml(i18n.t('profile.error_network', { message: err.message })) +
|
||||
escapeHtml(i18n.t('profile.error_network', { message: /** @type {Error} */ (err).message })) +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -162,10 +171,12 @@ async function changePassword(e) {
|
||||
|
||||
const AUTO_LABELS = ['Nextcloud', 'Nextcloud (OIDC)'];
|
||||
|
||||
/** @param {{label: string, active?: boolean, id: string}} pw */
|
||||
function isAutoPassword(pw) {
|
||||
return AUTO_LABELS.includes(pw.label);
|
||||
}
|
||||
|
||||
/** @param {{label: string, active?: boolean, id: string, created_at: string, last_used_at?: string}} pw */
|
||||
function renderPwRow(pw) {
|
||||
const tr = document.createElement('tr');
|
||||
const label = document.createElement('td');
|
||||
@@ -210,7 +221,9 @@ async function loadAppPasswords() {
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const passwords = data.app_passwords || data;
|
||||
const passwords = /** @type {Array<{label: string, active?: boolean, id: string, created_at: string, last_used_at?: string}>} */ (
|
||||
data.app_passwords || data
|
||||
);
|
||||
const userPws = passwords.filter((pw) => {
|
||||
return !isAutoPassword(pw);
|
||||
});
|
||||
@@ -236,7 +249,7 @@ async function loadAppPasswords() {
|
||||
autoSection.classList.add('hidden');
|
||||
} else {
|
||||
autoSection.classList.remove('hidden');
|
||||
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
|
||||
document.getElementById('app-pw-auto-count').textContent = String(autoPws.length);
|
||||
const autoTbody = document.getElementById('app-pw-auto-tbody');
|
||||
autoTbody.innerHTML = '';
|
||||
for (const pw of autoPws) autoTbody.appendChild(renderPwRow(pw));
|
||||
@@ -255,10 +268,10 @@ function toggleAutoPasswords() {
|
||||
}
|
||||
|
||||
async function createAppPassword() {
|
||||
const labelInput = document.getElementById('app-pw-label');
|
||||
const labelInput = /** @type {HTMLInputElement} */ (document.getElementById('app-pw-label'));
|
||||
const label = labelInput.value.trim();
|
||||
const statusEl = document.getElementById('app-pw-status');
|
||||
const btn = document.getElementById('app-pw-generate');
|
||||
const btn = /** @type {HTMLButtonElement} */ (document.getElementById('app-pw-generate'));
|
||||
|
||||
if (!label) {
|
||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.error_label_required'))}</div>`;
|
||||
@@ -291,7 +304,7 @@ async function createAppPassword() {
|
||||
labelInput.value = '';
|
||||
loadAppPasswords();
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
|
||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${/** @type {Error} */ (err).message}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(i18n.t('profile.generate'))}`;
|
||||
@@ -309,6 +322,10 @@ function copyAppPassword() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {string} label
|
||||
*/
|
||||
async function revokeAppPassword(id, label) {
|
||||
if (!confirm(i18n.t('profile.confirm_revoke', { label: label }))) return;
|
||||
try {
|
||||
@@ -325,10 +342,11 @@ async function revokeAppPassword(id, label) {
|
||||
alert(err.message || i18n.t('profile.error_revoke'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert(i18n.t('profile.error_network', { message: err.message }));
|
||||
alert(i18n.t('profile.error_network', { message: /** @type {Error} */ (err).message }));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
|
||||
@@ -17,12 +17,12 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
const $folder = document.getElementById('share-folder');
|
||||
|
||||
const $pwForm = document.getElementById('password-form');
|
||||
const $pwInput = document.getElementById('password-input');
|
||||
const $pwInput = /** @type {HTMLInputElement} */ (document.getElementById('password-input'));
|
||||
const $pwError = document.getElementById('password-error');
|
||||
|
||||
const $fileName = document.getElementById('file-name');
|
||||
const $fileMeta = document.getElementById('file-meta');
|
||||
const $fileDl = document.getElementById('file-download');
|
||||
const $fileDl = /** @type {HTMLAnchorElement} */ (document.getElementById('file-download'));
|
||||
const $expiredMsg = document.getElementById('expired-message');
|
||||
|
||||
// ── Token from URL path (/s/{token}) ──────────────────────────
|
||||
@@ -49,35 +49,49 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
}
|
||||
let rootDisplayName = 'Shared folder';
|
||||
|
||||
/**
|
||||
* @param {'loading'|'password'|'expired'|'file'|'folder'} name
|
||||
*/
|
||||
function showState(name) {
|
||||
for (const el of [$loading, $password, $expired, $file, $folder]) {
|
||||
if (el) el.classList.add('hidden');
|
||||
}
|
||||
const target = {
|
||||
/** @type {{ loading: HTMLElement|null, password: HTMLElement|null, expired: HTMLElement|null, file: HTMLElement|null, folder: HTMLElement|null }} */
|
||||
const map = {
|
||||
loading: $loading,
|
||||
password: $password,
|
||||
expired: $expired,
|
||||
file: $file,
|
||||
folder: $folder
|
||||
}[name];
|
||||
};
|
||||
const target = map[name];
|
||||
if (target) target.classList.remove('hidden');
|
||||
document.body.classList.toggle('gallery-mode', name === 'folder');
|
||||
}
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────────────
|
||||
/**
|
||||
* @param {string|null|undefined} s
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(
|
||||
/[&<>"']/g,
|
||||
(c) =>
|
||||
/** @type {Record<string, string>} */
|
||||
({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[c]
|
||||
})[c] ?? c
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {number} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatSize(bytes) {
|
||||
if (bytes == null || Number.isNaN(bytes)) return '';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
@@ -90,6 +104,10 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
}
|
||||
return `${v < 10 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
|
||||
}
|
||||
/**
|
||||
* @param {string|null|undefined} mime
|
||||
* @returns {'image'|'video'|null}
|
||||
*/
|
||||
function mediaKind(mime) {
|
||||
const m = (mime || '').toLowerCase();
|
||||
if (m.startsWith('image/')) return 'image';
|
||||
@@ -97,6 +115,9 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
return null;
|
||||
}
|
||||
// ── Render share data ─────────────────────────────────────────
|
||||
/**
|
||||
* @param {any} data
|
||||
*/
|
||||
function renderShare(data) {
|
||||
if (data.item_type === 'folder') {
|
||||
rootDisplayName = data.item_name || 'Shared folder';
|
||||
@@ -114,13 +135,15 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
.then((res) => {
|
||||
if (res.ok) return res.json();
|
||||
if (res.status === 401) {
|
||||
return res.json().then((body) => {
|
||||
if (body?.requiresPassword) {
|
||||
showState('password');
|
||||
return null;
|
||||
return res.json().then(
|
||||
/** @type {(body:any) => null} */ (body) => {
|
||||
if (body?.requiresPassword) {
|
||||
showState('password');
|
||||
return null;
|
||||
}
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
throw new Error('Unauthorized');
|
||||
});
|
||||
);
|
||||
}
|
||||
if (res.status === 410) {
|
||||
showState('expired');
|
||||
@@ -171,7 +194,9 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
|
||||
// ── Folder gallery ────────────────────────────────────────────
|
||||
|
||||
/** @type {string | null} */
|
||||
let currentFolderId = null;
|
||||
/** @type {string | null} */
|
||||
let currentFolderName = null;
|
||||
|
||||
function initFolderGallery() {
|
||||
@@ -195,17 +220,33 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | null} folderId
|
||||
* @returns {string}
|
||||
*/
|
||||
function listingUrl(folderId) {
|
||||
return folderId ? `/api/s/${TOKEN_ENC}/contents/${encodeURIComponent(folderId)}` : `/api/s/${TOKEN_ENC}/contents`;
|
||||
}
|
||||
/**
|
||||
* @param {string} fileId
|
||||
* @returns {string}
|
||||
*/
|
||||
function fileUrl(fileId) {
|
||||
return `/api/s/${TOKEN_ENC}/file/${encodeURIComponent(fileId)}`;
|
||||
}
|
||||
/**
|
||||
* @param {string | null} folderId
|
||||
* @returns {string}
|
||||
*/
|
||||
function zipUrl(folderId) {
|
||||
return folderId ? `/api/s/${TOKEN_ENC}/zip/${encodeURIComponent(folderId)}` : `/api/s/${TOKEN_ENC}/zip`;
|
||||
}
|
||||
|
||||
/** @type {AbortController | null} */
|
||||
let currentLoadController = null;
|
||||
/**
|
||||
* @param {string | null} folderId
|
||||
*/
|
||||
function loadAndRender(folderId) {
|
||||
if (currentLoadController) currentLoadController.abort();
|
||||
const controller = new AbortController();
|
||||
@@ -236,6 +277,10 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} listing
|
||||
* @param {string | null} folderId
|
||||
*/
|
||||
function renderGallery(listing, folderId) {
|
||||
const isSubfolder = folderId !== null;
|
||||
const title = isSubfolder ? currentFolderName || 'Subfolder' : rootDisplayName;
|
||||
@@ -252,7 +297,7 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
|
||||
const filesHtml =
|
||||
listing.files && listing.files.length > 0
|
||||
? `<h3 class="gallery-section-title">Files</h3><div class="gallery-files">${listing.files.map((f) => fileCardHtml(f)).join('')}</div>`
|
||||
? `<h3 class="gallery-section-title">Files</h3><div class="gallery-files">${listing.files.map((/** @type {any} */ f) => fileCardHtml(f)).join('')}</div>`
|
||||
: '';
|
||||
|
||||
const emptyHtml = empty ? '<div class="gallery-empty">This folder is empty.</div>' : '';
|
||||
@@ -265,10 +310,18 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
wireImageRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} folder
|
||||
* @returns {string}
|
||||
*/
|
||||
function folderCardHtml(folder) {
|
||||
return `<a class="folder-card" href="#" data-action="open-folder" data-id="${escapeHtml(folder.id)}" data-name="${escapeHtml(folder.name)}"><i class="fas fa-folder folder-icon"></i><div class="card-body"><div class="card-name">${escapeHtml(folder.name)}</div><div class="card-meta">Subfolder</div></div></a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} file
|
||||
* @returns {string}
|
||||
*/
|
||||
function fileCardHtml(file) {
|
||||
const url = fileUrl(file.id);
|
||||
const kind = mediaKind(file.mime_type);
|
||||
@@ -286,7 +339,7 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
}
|
||||
|
||||
function wireGallery() {
|
||||
for (const btn of $folder.querySelectorAll('.gallery-view-toggle button')) {
|
||||
for (const btn of /** @type {NodeListOf<HTMLButtonElement>} */ ($folder.querySelectorAll('.gallery-view-toggle button'))) {
|
||||
btn.addEventListener('click', () => setViewMode(btn.dataset.view));
|
||||
}
|
||||
const backBtn = $folder.querySelector('[data-action="back"]');
|
||||
@@ -296,14 +349,15 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
navigate(null, rootDisplayName);
|
||||
});
|
||||
}
|
||||
for (const card of $folder.querySelectorAll('[data-action="open-folder"]')) {
|
||||
for (const card of /** @type {NodeListOf<HTMLDivElement>} */ ($folder.querySelectorAll('[data-action="open-folder"]'))) {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (!(e instanceof MouseEvent)) return;
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
navigate(card.dataset.id, card.dataset.name);
|
||||
});
|
||||
}
|
||||
const mediaCards = Array.from($folder.querySelectorAll('.file-card[data-mediakind]'));
|
||||
const mediaCards = Array.from(/** @type {NodeListOf<HTMLDivElement>} */ ($folder.querySelectorAll('.file-card[data-mediakind]')));
|
||||
const items = mediaCards.map((el) => ({
|
||||
kind: el.dataset.mediakind,
|
||||
src: el.dataset.src,
|
||||
@@ -318,6 +372,9 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | null | undefined} v
|
||||
*/
|
||||
function setViewMode(v) {
|
||||
const mode = v === 'list' ? 'list' : 'grid';
|
||||
viewMode = mode;
|
||||
@@ -327,11 +384,15 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
// ignore
|
||||
}
|
||||
document.body.dataset.shareView = mode;
|
||||
for (const b of $folder.querySelectorAll('.gallery-view-toggle button')) {
|
||||
for (const b of /** @type {NodeListOf<HTMLButtonElement>} */ ($folder.querySelectorAll('.gallery-view-toggle button'))) {
|
||||
b.setAttribute('aria-pressed', String(b.dataset.view === mode));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | null} folderId
|
||||
* @param {string | null | undefined} folderName
|
||||
*/
|
||||
function navigate(folderId, folderName) {
|
||||
currentFolderId = folderId;
|
||||
currentFolderName = folderName;
|
||||
@@ -352,7 +413,7 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
function wireLazyVideos() {
|
||||
const lazy = $folder.querySelectorAll('.file-thumb video[data-lazy-src]');
|
||||
if (!lazy.length) return;
|
||||
const start = (v) => {
|
||||
const start = (/** @type {HTMLVideoElement} */ v) => {
|
||||
v.addEventListener(
|
||||
'loadedmetadata',
|
||||
() => {
|
||||
@@ -385,20 +446,21 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
(entries) => {
|
||||
for (const e of entries) {
|
||||
if (!e.isIntersecting) continue;
|
||||
if (e.target.dataset.lazySrc && !e.target.src) start(e.target);
|
||||
obs.unobserve(e.target);
|
||||
const target = /** @type {HTMLVideoElement} */ (e.target);
|
||||
if (target.dataset.lazySrc && !target.src) start(target);
|
||||
obs.unobserve(target);
|
||||
}
|
||||
},
|
||||
{ rootMargin: '300px' }
|
||||
);
|
||||
for (const v of lazy) obs.observe(v);
|
||||
} else {
|
||||
for (const v of lazy) start(v);
|
||||
for (const v of lazy) start(/** @type {HTMLVideoElement} */ (v));
|
||||
}
|
||||
}
|
||||
|
||||
function wireImageRetry() {
|
||||
for (const img of $folder.querySelectorAll('.file-thumb img')) {
|
||||
for (const img of /** @type {NodeListOf<HTMLImageElement>} */ ($folder.querySelectorAll('.file-thumb img'))) {
|
||||
img.addEventListener('error', () => {
|
||||
if (img.dataset.retried === '1') return;
|
||||
img.dataset.retried = '1';
|
||||
@@ -412,7 +474,12 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
}
|
||||
|
||||
// ── Lightbox ──────────────────────────────────────────────────
|
||||
/**
|
||||
* @typedef {{ root: HTMLElement, title: Element|null, download: HTMLAnchorElement|null, close: HTMLButtonElement|null, stage: Element|null, content: Element|null, prev: HTMLButtonElement|null, next: HTMLButtonElement|null }} LightboxRefs
|
||||
*/
|
||||
/** @type {LightboxRefs | null} */
|
||||
let lb = null;
|
||||
/** @type {Array<{kind: string|undefined, src: string|undefined, name: string|undefined}>} */
|
||||
let lbItems = [];
|
||||
let lbIndex = -1;
|
||||
|
||||
@@ -466,11 +533,18 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
return lb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{kind: string|undefined, src: string|undefined, name: string|undefined}>} items
|
||||
* @param {number} index
|
||||
*/
|
||||
function openLightbox(items, index) {
|
||||
ensureLightbox();
|
||||
lbItems = items;
|
||||
showLightboxItem(index);
|
||||
}
|
||||
/**
|
||||
* @param {number} i
|
||||
*/
|
||||
function showLightboxItem(i) {
|
||||
if (i < 0 || i >= lbItems.length) return;
|
||||
lbIndex = i;
|
||||
@@ -497,6 +571,9 @@ import { uiFileTypes } from '../../app/uiFileTypes.js';
|
||||
lb.root.classList.remove('hidden');
|
||||
lb.root.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
/**
|
||||
* @param {number} delta
|
||||
*/
|
||||
function stepLightbox(delta) {
|
||||
const next = lbIndex + delta;
|
||||
if (next >= 0 && next < lbItems.length) showLightboxItem(next);
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
import { switchToFilesSection } from '../../app/navigation.js';
|
||||
import { ui } from '../../app/ui.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { formatDateShort } from '../../core/formatters.js';
|
||||
import { formatDateShort, isEmailValid } from '../../core/formatters.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { fileSharing } from '../../features/sharing/fileSharing.js';
|
||||
|
||||
/** @import {Share} from '../../core/types.js' */
|
||||
/** @import {ShareItem} from '../../core/types.js' */
|
||||
|
||||
const TTL = 5 * 60 * 1000; // 5 min
|
||||
|
||||
const sharedView = {
|
||||
// State
|
||||
|
||||
/** @type {Array<Share>} */
|
||||
/** @type {Array<ShareItem>} */
|
||||
items: [],
|
||||
|
||||
_expires: 0,
|
||||
@@ -25,8 +25,10 @@ const sharedView = {
|
||||
/** @type {Map<string, boolean>} key = "file:<id>" | "folder:<id>" */
|
||||
_knownItemsId: new Map(),
|
||||
|
||||
/** @type {Array<Share>} */
|
||||
/** @type {Array<ShareItem>} */
|
||||
filteredItems: [],
|
||||
|
||||
/** @type {ShareItem | null} */
|
||||
currentItem: null,
|
||||
|
||||
/** Auth header helper — tokens are in HttpOnly cookies now */
|
||||
@@ -238,6 +240,7 @@ const sharedView = {
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach((sel) => {
|
||||
if (!(e.target instanceof Node)) return;
|
||||
if (!sel.contains(e.target)) sel.classList.remove('open');
|
||||
});
|
||||
});
|
||||
@@ -249,8 +252,8 @@ const sharedView = {
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.closeShareDialog());
|
||||
const copyLinkBtn = document.getElementById('sv-copy-link-btn');
|
||||
if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink());
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
|
||||
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
|
||||
if (enablePw)
|
||||
enablePw.addEventListener('change', () => {
|
||||
if (pwField) {
|
||||
@@ -260,8 +263,8 @@ const sharedView = {
|
||||
});
|
||||
const genPwBtn = document.getElementById('sv-generate-password');
|
||||
if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword());
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration'));
|
||||
const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration'));
|
||||
if (enableExp)
|
||||
enableExp.addEventListener('change', () => {
|
||||
if (expField) {
|
||||
@@ -294,6 +297,13 @@ const sharedView = {
|
||||
},
|
||||
|
||||
// Initialize a custom select dropdown
|
||||
/**
|
||||
*
|
||||
* @param {string} wrapperId
|
||||
* @param {string} toggleId
|
||||
* @param {string} dropdownId
|
||||
* @returns
|
||||
*/
|
||||
_initCustomSelect(wrapperId, toggleId, dropdownId) {
|
||||
const wrapper = document.getElementById(wrapperId);
|
||||
const toggle = document.getElementById(toggleId);
|
||||
@@ -330,14 +340,14 @@ const sharedView = {
|
||||
|
||||
// Filter and sort items
|
||||
filterAndSortItems() {
|
||||
const filterTypeActive = document.querySelector('#filter-type-dropdown .shared-select-option.active');
|
||||
const sortByActive = document.querySelector('#sort-by-dropdown .shared-select-option.active');
|
||||
const filterTypeActive = /** @type {HTMLDivElement} */ (document.querySelector('#filter-type-dropdown .shared-select-option.active'));
|
||||
const sortByActive = /** @type {HTMLDivElement} */ (document.querySelector('#sort-by-dropdown .shared-select-option.active'));
|
||||
|
||||
const type = filterTypeActive ? filterTypeActive.dataset.value : 'all';
|
||||
const sort = sortByActive ? sortByActive.dataset.value : 'date';
|
||||
|
||||
// Use the main top-bar search input
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchInput = /** @type {HTMLInputElement} */ (document.getElementById('search-input'));
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
|
||||
this.filteredItems = this.items.filter((item) => {
|
||||
@@ -396,23 +406,23 @@ const sharedView = {
|
||||
nameCell.appendChild(nameSpan);
|
||||
|
||||
const typeCell = document.createElement('td');
|
||||
typeCell.textContent = item.item_type === 'file' ? this.translate('shared_typeFile', 'File') : this.translate('shared_typeFolder', 'Folder');
|
||||
typeCell.textContent = item.item_type === 'file' ? i18n.t('shared_typeFile', 'File') : i18n.t('shared_typeFolder', 'Folder');
|
||||
|
||||
const dateCell = document.createElement('td');
|
||||
dateCell.textContent = this.formatDate(item.created_at);
|
||||
dateCell.textContent = formatDateShort(item.created_at);
|
||||
|
||||
const expCell = document.createElement('td');
|
||||
expCell.textContent = item.expires_at ? this.formatDate(item.expires_at) : this.translate('shared_noExpiration', 'No expiration');
|
||||
expCell.textContent = item.expires_at ? formatDateShort(item.expires_at) : i18n.t('shared_noExpiration', 'No expiration');
|
||||
|
||||
const permCell = document.createElement('td');
|
||||
const perms = [];
|
||||
if (item.permissions?.read) perms.push(this.translate('share_permissionRead', 'Read'));
|
||||
if (item.permissions?.write) perms.push(this.translate('share_permissionWrite', 'Write'));
|
||||
if (item.permissions?.reshare) perms.push(this.translate('share_permissionReshare', 'Reshare'));
|
||||
if (item.permissions?.read) perms.push(i18n.t('share_permissionRead', 'Read'));
|
||||
if (item.permissions?.write) perms.push(i18n.t('share_permissionWrite', 'Write'));
|
||||
if (item.permissions?.reshare) perms.push(i18n.t('share_permissionReshare', 'Reshare'));
|
||||
permCell.textContent = perms.join(', ') || 'Read';
|
||||
|
||||
const pwCell = document.createElement('td');
|
||||
pwCell.textContent = item.has_password ? this.translate('shared_hasPassword', 'Yes') : this.translate('shared_noPassword', 'No');
|
||||
pwCell.textContent = item.has_password ? i18n.t('shared_hasPassword', 'Yes') : i18n.t('shared_noPassword', 'No');
|
||||
|
||||
const actionsCell = document.createElement('td');
|
||||
actionsCell.className = 'shared-item-actions';
|
||||
@@ -420,30 +430,30 @@ const sharedView = {
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'action-btn edit-btn';
|
||||
editBtn.innerHTML = '<span class="action-icon">✏️</span>';
|
||||
editBtn.title = this.translate('shared_editShare', 'Edit Share');
|
||||
editBtn.title = i18n.t('shared_editShare', 'Edit Share');
|
||||
editBtn.addEventListener('click', () => this.openShareDialog(item));
|
||||
|
||||
const notifyBtn = document.createElement('button');
|
||||
notifyBtn.className = 'action-btn notify-btn';
|
||||
notifyBtn.innerHTML = '<span class="action-icon">📧</span>';
|
||||
notifyBtn.title = this.translate('shared_notifyShare', 'Notify Someone');
|
||||
notifyBtn.title = i18n.t('shared_notifyShare', 'Notify Someone');
|
||||
notifyBtn.addEventListener('click', () => this.openNotificationDialog(item));
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn copy-btn';
|
||||
copyBtn.innerHTML = '<span class="action-icon">📋</span>';
|
||||
copyBtn.title = this.translate('shared_copyLink', 'Copy Link');
|
||||
copyBtn.title = i18n.t('shared_copyLink', 'Copy Link');
|
||||
copyBtn.addEventListener('click', () => {
|
||||
navigator.clipboard
|
||||
.writeText(item.url)
|
||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||
.then(() => ui.showNotification(i18n.t('shared_linkCopied', 'Link copied!'), 'success'))
|
||||
.catch(() => ui.showNotification(i18n.t('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||
});
|
||||
|
||||
const rmBtn = document.createElement('button');
|
||||
rmBtn.className = 'action-btn remove-btn';
|
||||
rmBtn.innerHTML = '<span class="action-icon">🗑️</span>';
|
||||
rmBtn.title = this.translate('shared_removeShare', 'Remove Share');
|
||||
rmBtn.title = i18n.t('shared_removeShare', 'Remove Share');
|
||||
rmBtn.addEventListener('click', () => {
|
||||
this.currentItem = item;
|
||||
this.removeSharedItem();
|
||||
@@ -456,6 +466,11 @@ const sharedView = {
|
||||
},
|
||||
|
||||
// Open share dialog
|
||||
/**
|
||||
*
|
||||
* @param {ShareItem} item
|
||||
* @returns {void}
|
||||
*/
|
||||
openShareDialog(item) {
|
||||
this.currentItem = item;
|
||||
const shareDialog = document.getElementById('shared-view-edit-dialog');
|
||||
@@ -463,14 +478,14 @@ const sharedView = {
|
||||
|
||||
const iconEl = document.getElementById('sv-dialog-icon');
|
||||
const nameEl = document.getElementById('sv-dialog-name');
|
||||
const urlEl = document.getElementById('sv-share-link-url');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
const permRead = document.getElementById('sv-permission-read');
|
||||
const permWrite = document.getElementById('sv-permission-write');
|
||||
const permReshare = document.getElementById('sv-permission-reshare');
|
||||
const urlEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-link-url'));
|
||||
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
|
||||
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
|
||||
const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration'));
|
||||
const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration'));
|
||||
const permRead = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-read'));
|
||||
const permWrite = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-write'));
|
||||
const permReshare = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-reshare'));
|
||||
|
||||
if (!shareDialog) return;
|
||||
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
|
||||
@@ -505,14 +520,19 @@ const sharedView = {
|
||||
this.currentItem = null;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ShareItem} item
|
||||
* @returns {void}
|
||||
*/
|
||||
openNotificationDialog(item) {
|
||||
this.currentItem = item;
|
||||
const dn = item.item_name || item.item_id || 'Unknown';
|
||||
const d = document.getElementById('sv-notification-dialog');
|
||||
const iconEl = document.getElementById('sv-notify-dialog-icon');
|
||||
const nameEl = document.getElementById('sv-notify-dialog-name');
|
||||
const emailEl = document.getElementById('sv-notification-email');
|
||||
const msgEl = document.getElementById('sv-notification-message');
|
||||
const emailEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-email'));
|
||||
const msgEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-message'));
|
||||
|
||||
if (!d) return;
|
||||
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
|
||||
@@ -529,18 +549,18 @@ const sharedView = {
|
||||
},
|
||||
|
||||
copyShareLink() {
|
||||
const el = document.getElementById('sv-share-link-url');
|
||||
const el = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-link-url'));
|
||||
if (!el) return;
|
||||
navigator.clipboard
|
||||
.writeText(el.value)
|
||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||
.then(() => ui.showNotification(i18n.t('shared_linkCopied', 'Link copied!'), 'success'))
|
||||
.catch(() => ui.showNotification(i18n.t('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
|
||||
},
|
||||
|
||||
// Generate secure password with crypto API
|
||||
generatePassword() {
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
|
||||
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
|
||||
if (!pwField || !enablePw) return;
|
||||
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
@@ -559,13 +579,13 @@ const sharedView = {
|
||||
async updateSharedItem() {
|
||||
if (!this.currentItem) return;
|
||||
|
||||
const permRead = document.getElementById('sv-permission-read');
|
||||
const permWrite = document.getElementById('sv-permission-write');
|
||||
const permReshare = document.getElementById('sv-permission-reshare');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
const permRead = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-read'));
|
||||
const permWrite = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-write'));
|
||||
const permReshare = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-reshare'));
|
||||
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
|
||||
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
|
||||
const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration'));
|
||||
const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration'));
|
||||
|
||||
const body = {
|
||||
permissions: {
|
||||
@@ -588,10 +608,10 @@ const sharedView = {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `Server error ${res.status}`);
|
||||
}
|
||||
this.showNotification(this.translate('shared_itemUpdated', 'Share settings updated'));
|
||||
ui.showNotification(i18n.t('shared_itemUpdated', 'Share settings updated'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Error updating share:', err);
|
||||
this.showNotification(err.message || 'Error updating share', 'error');
|
||||
ui.showNotification(/** @type {Error} */ (err).message || 'Error updating share', 'error');
|
||||
}
|
||||
// update UI
|
||||
ui.setSharedVisualState(this.currentItem.item_id, this.currentItem.item_type, true);
|
||||
@@ -611,10 +631,10 @@ const sharedView = {
|
||||
headers: this._headers()
|
||||
});
|
||||
if (!res.ok && res.status !== 204) throw new Error(`Server error ${res.status}`);
|
||||
this.showNotification(this.translate('shared_itemRemoved', 'Share removed'));
|
||||
ui.showNotification(i18n.t('shared_itemRemoved', 'Share removed'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Error removing share:', err);
|
||||
this.showNotification('Error removing share', 'error');
|
||||
ui.showNotification('Error removing share', 'error');
|
||||
}
|
||||
|
||||
this.closeShareDialog();
|
||||
@@ -627,13 +647,13 @@ const sharedView = {
|
||||
// Send notification (stub)
|
||||
sendNotification() {
|
||||
if (!this.currentItem) return;
|
||||
const emailEl = document.getElementById('sv-notification-email');
|
||||
const msgEl = document.getElementById('sv-notification-message');
|
||||
const emailEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-email'));
|
||||
const msgEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-message'));
|
||||
const email = emailEl ? emailEl.value.trim() : '';
|
||||
const message = msgEl ? msgEl.value.trim() : '';
|
||||
|
||||
if (!email || !this.validateEmail(email)) {
|
||||
this.showNotification(this.translate('shared_invalidEmail', 'Please enter a valid email address'), 'error');
|
||||
if (!email || !isEmailValid(email)) {
|
||||
ui.showNotification(i18n.t('shared_invalidEmail', 'Please enter a valid email address'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -642,30 +662,10 @@ const sharedView = {
|
||||
.sendShareNotification(this.currentItem.url, email, message)
|
||||
.then(() => {
|
||||
this.closeNotificationDialog();
|
||||
this.showNotification(this.translate('shared_notificationSent', 'Notification sent'));
|
||||
ui.showNotification(i18n.t('shared_notificationSent', 'Notification sent'), 'success');
|
||||
})
|
||||
.catch(() => this.showNotification(this.translate('shared_notificationFailed', 'Failed to send notification'), 'error'));
|
||||
.catch(() => ui.showNotification(i18n.t('shared_notificationFailed', 'Failed to send notification'), 'error'));
|
||||
}
|
||||
},
|
||||
|
||||
showNotification(message, type = 'success') {
|
||||
if (ui?.showNotification) {
|
||||
ui.showNotification(message, type);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
},
|
||||
|
||||
validateEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
},
|
||||
|
||||
formatDate(value) {
|
||||
return formatDateShort(value);
|
||||
},
|
||||
|
||||
translate(key, defaultText) {
|
||||
return i18n.t(key, defaultText);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user