fix: folder trash/delete operations & frontend refactoring

- Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' does not exist)
- Fix folder deletion: delete descendant files before folder to avoid 'duplicate key violates unique constraint idx_files_unique_name_at_root'
- Simplify trash model: only mark the folder as trashed, not child files (implicit trash via parent)
- Update trash_items view: filter to show only top-level trashed items
- Update schema.sql: change files.folder_id FK from ON DELETE SET NULL to ON DELETE CASCADE
- Fix trash view icons: folders and files now show correct visual icons (folder-icon, pdf-icon, etc.) in trash view
- Frontend refactoring: extract inline CSS/JS from admin.html and profile.html into dedicated external files
- Frontend cleanup: replace all inline style attributes with CSS classes
- Frontend cleanup: replace style.display JS
- Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' d
This commit is contained in:
Diocrafts
2026-02-20 12:27:52 +01:00
parent 27eb7b16e0
commit a1a3bd1b2b
43 changed files with 3304 additions and 3416 deletions
+397
View File
@@ -0,0 +1,397 @@
const API = '/api';
const token = localStorage.getItem('oxicloud_token') || localStorage.getItem('token') || localStorage.getItem('access_token');
let currentAdminId = '';
let usersPage = 0;
const PAGE_SIZE = 50;
let totalUsers = 0;
function hideElement(id) {
const element = document.getElementById(id);
if (!element) return;
element.classList.remove('show-block', 'show-flex');
element.classList.add('hidden');
}
function showElement(id, mode = 'block') {
const element = document.getElementById(id);
if (!element) return;
element.classList.remove('hidden', 'show-block', 'show-flex');
if (mode === 'flex') {
element.classList.add('show-flex');
} else {
element.classList.add('show-block');
}
}
function headers() {
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function timeAgo(dateStr) {
if (!dateStr) return 'Never';
const d = new Date(dateStr);
const now = new Date();
const secs = Math.floor((now - d) / 1000);
if (secs < 60) return 'Just now';
if (secs < 3600) return Math.floor(secs/60) + 'm ago';
if (secs < 86400) return Math.floor(secs/3600) + 'h ago';
if (secs < 2592000) return Math.floor(secs/86400) + 'd ago';
return d.toLocaleDateString();
}
function switchTab(name, el) {
document.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
if (el) el.classList.add('active');
if (name === 'users') loadUsers();
if (name === 'dashboard') loadDashboard();
}
async function loadDashboard() {
try {
const resp = await fetch(API + '/admin/dashboard', { headers: headers() });
if (!resp.ok) return;
const d = await resp.json();
document.getElementById('ds-total-users').textContent = d.total_users;
document.getElementById('ds-active-users').textContent = d.active_users;
document.getElementById('ds-admin-users').textContent = d.admin_users;
document.getElementById('ds-version').textContent = 'v' + d.server_version;
document.getElementById('ds-used').textContent = formatBytes(d.total_used_bytes);
document.getElementById('ds-quota').textContent = formatBytes(d.total_quota_bytes);
document.getElementById('ds-usage-pct').textContent = d.storage_usage_percent.toFixed(1) + '%';
const bar = document.getElementById('ds-bar');
bar.style.width = Math.min(d.storage_usage_percent, 100) + '%';
bar.className = 'progress-fill ' + (d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green');
document.getElementById('ds-auth').textContent = d.auth_enabled ? 'Enabled' : 'Disabled';
document.getElementById('ds-oidc').textContent = d.oidc_configured ? 'Active' : 'Off';
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? 'Enabled' : 'Disabled';
if (typeof d.registration_enabled !== 'undefined') {
document.getElementById('ds-registration').checked = d.registration_enabled;
if (d.registration_enabled) hideElement('registration-warning');
else showElement('registration-warning', 'flex');
}
if (d.users_over_80_percent > 0) {
showElement('ds-warn-card');
document.getElementById('ds-over80').textContent = d.users_over_80_percent;
}
if (d.users_over_quota > 0) {
showElement('ds-danger-card');
document.getElementById('ds-overquota').textContent = d.users_over_quota;
}
} catch (e) { console.error('Dashboard error', e); }
}
async function loadUsers() {
const tbody = document.getElementById('users-tbody');
tbody.innerHTML = '<tr><td colspan="6" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> Loading…</td></tr>';
try {
const resp = await fetch(API + '/admin/users?limit=' + PAGE_SIZE + '&offset=' + (usersPage * PAGE_SIZE), { headers: headers() });
if (!resp.ok) { tbody.innerHTML = '<tr><td colspan="6" class="table-status-error"><i class="fas fa-exclamation-circle"></i> Failed to load users</td></tr>'; return; }
const data = await resp.json();
totalUsers = data.total;
const users = data.users;
if (users.length === 0) { tbody.innerHTML = '<tr><td colspan="6" class="table-status-empty">No users found</td></tr>'; return; }
tbody.innerHTML = users.map(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 = u.storage_quota_bytes > 0 ? formatBytes(u.storage_used_bytes) + ' / ' + formatBytes(u.storage_quota_bytes) : formatBytes(u.storage_used_bytes) + ' / ∞';
const isSelf = u.id === currentAdminId;
return '<tr>' +
'<td><div class="user-info"><span class="user-name">' + u.username + (isSelf ? ' <span class="user-self-badge">(you)</span>' : '') + '</span><span class="user-email">' + u.email + '</span></div></td>' +
'<td><span class="badge badge-' + u.role + '">' + (u.role === 'admin' ? '<i class="fas fa-shield-alt badge-admin-icon-small"></i> ' : '') + u.role + '</span></td>' +
'<td><span class="badge badge-' + (u.active ? 'active' : 'inactive') + '">' + (u.active ? 'Active' : 'Inactive') + '</span></td>' +
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + quotaColor + '" style="width:' + Math.min(quotaPct, 100) + '%"></div></div><span class="quota-text">' + quotaText + '</span></div></td>' +
'<td class="user-last-login-cell">' + timeAgo(u.last_login_at) + '</td>' +
'<td><div class="actions-row">' +
'<button class="btn btn-sm btn-secondary" onclick="openQuotaModal(\'' + u.id + '\',\'' + u.username + '\',' + u.storage_quota_bytes + ')" title="Edit quota"><i class="fas fa-box"></i></button>' +
'<button class="btn btn-sm btn-secondary" onclick="openResetPasswordModal(\'' + u.id + '\',\'' + u.username + '\')" title="Reset password"><i class="fas fa-key"></i></button>' +
'<button class="btn btn-sm btn-secondary" onclick="toggleRole(\'' + u.id + '\',\'' + u.role + '\')" title="Toggle role"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-' + (u.role === 'admin' ? 'user' : 'crown') + '"></i></button>' +
'<button class="btn btn-sm ' + (u.active ? 'btn-danger' : 'btn-success') + '" onclick="toggleActive(\'' + u.id + '\',' + u.active + ')" title="' + (u.active ? 'Deactivate' : 'Activate') + '"' + (isSelf && u.active ? ' disabled' : '') + '><i class="fas fa-' + (u.active ? 'ban' : 'check') + '"></i></button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteUser(\'' + u.id + '\',\'' + u.username + '\')" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
'</div></td></tr>';
}).join('');
document.getElementById('users-info').textContent = 'Showing ' + (usersPage * PAGE_SIZE + 1) + '-' + Math.min((usersPage + 1) * PAGE_SIZE, totalUsers) + ' of ' + totalUsers;
document.getElementById('prev-btn').disabled = usersPage === 0;
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
} catch (e) {
tbody.innerHTML = '<tr><td colspan="6" class="table-status-error"><i class="fas fa-exclamation-circle"></i> Error: ' + e.message + '</td></tr>';
}
}
function prevPage() { if (usersPage > 0) { usersPage--; loadUsers(); } }
function nextPage() { if ((usersPage + 1) * PAGE_SIZE < totalUsers) { usersPage++; loadUsers(); } }
async function toggleRole(userId, currentRole) {
const newRole = currentRole === 'admin' ? 'user' : 'admin';
if (!confirm('Change role to ' + newRole + '?')) return;
try {
const resp = await fetch(API + '/admin/users/' + userId + '/role', {
method: 'PUT', headers: headers(), body: JSON.stringify({ role: newRole })
});
if (resp.ok) loadUsers(); else { const e = await resp.json(); alert(e.message || 'Failed'); }
} catch (e) { alert('Error: ' + e.message); }
}
async function toggleActive(userId, currentActive) {
const action = currentActive ? 'deactivate' : 'activate';
if (!confirm('Are you sure you want to ' + action + ' this user?')) return;
try {
const resp = await fetch(API + '/admin/users/' + userId + '/active', {
method: 'PUT', headers: headers(), body: JSON.stringify({ active: !currentActive })
});
if (resp.ok) loadUsers(); else { const e = await resp.json(); alert(e.message || 'Failed'); }
} catch (e) { alert('Error: ' + e.message); }
}
async function deleteUser(userId, username) {
if (!confirm('DELETE user "' + username + '"? This cannot be undone!')) return;
try {
const resp = await fetch(API + '/admin/users/' + userId, { method: 'DELETE', headers: headers() });
if (resp.ok) { loadUsers(); loadDashboard(); } else { const e = await resp.json(); alert(e.message || 'Failed'); }
} catch (e) { alert('Error: ' + e.message); }
}
let quotaUserId = '';
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;
showElement('quota-modal', 'flex');
}
function closeQuotaModal() { hideElement('quota-modal'); }
async function saveQuota() {
const val = parseFloat(document.getElementById('qm-value').value) || 0;
const unit = parseInt(document.getElementById('qm-unit').value);
const bytes = Math.round(val * unit);
try {
const resp = await fetch(API + '/admin/users/' + quotaUserId + '/quota', {
method: 'PUT', headers: headers(), body: JSON.stringify({ quota_bytes: bytes })
});
if (resp.ok) { closeQuotaModal(); loadUsers(); loadDashboard(); }
else { const e = await resp.json(); alert(e.message || 'Failed'); }
} catch (e) { alert('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';
document.getElementById('cu-error').className = 'alert';
document.getElementById('cu-error').textContent = '';
showElement('create-user-modal', 'flex');
setTimeout(() => 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);
const quotaBytes = Math.round(quotaVal * quotaUnit);
const errorEl = document.getElementById('cu-error');
if (username.length < 3) { errorEl.textContent = 'Username must be at least 3 characters'; errorEl.className = 'alert alert-error'; return; }
if (password.length < 8) { errorEl.textContent = 'Password must be at least 8 characters'; errorEl.className = 'alert alert-error'; return; }
const btn = document.getElementById('cu-submit');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating…';
try {
const resp = await fetch(API + '/admin/users', {
method: 'POST', headers: headers(),
body: JSON.stringify({ username, password, email, role, quota_bytes: quotaBytes })
});
if (resp.ok) {
closeCreateUserModal();
loadUsers();
loadDashboard();
} else {
const e = await resp.json().catch(() => ({}));
errorEl.textContent = e.message || 'Failed to create user';
errorEl.className = 'alert alert-error';
}
} catch (e) {
errorEl.textContent = 'Network error: ' + e.message;
errorEl.className = 'alert alert-error';
}
btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus"></i> Create';
}
let resetPwUserId = '';
function openResetPasswordModal(userId, username) {
resetPwUserId = userId;
document.getElementById('rp-username').textContent = username;
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);
}
function closeResetPasswordModal() { hideElement('reset-pw-modal'); }
async function submitResetPassword() {
const password = document.getElementById('rp-password').value;
const errorEl = document.getElementById('rp-error');
if (password.length < 8) { errorEl.textContent = 'Password must be at least 8 characters'; errorEl.className = 'alert alert-error'; return; }
const btn = document.getElementById('rp-submit');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Resetting…';
try {
const resp = await fetch(API + '/admin/users/' + resetPwUserId + '/password', {
method: 'PUT', headers: headers(),
body: JSON.stringify({ new_password: password })
});
if (resp.ok) { closeResetPasswordModal(); }
else { const e = await resp.json().catch(() => ({})); errorEl.textContent = e.message || 'Failed'; errorEl.className = 'alert alert-error'; }
} catch (e) { errorEl.textContent = 'Error: ' + e.message; errorEl.className = 'alert alert-error'; }
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> Reset';
}
async function toggleRegistration(enabled) {
if (enabled) hideElement('registration-warning');
else showElement('registration-warning', 'flex');
try {
const resp = await fetch(API + '/admin/settings/registration', {
method: 'PUT', headers: headers(),
body: JSON.stringify({ registration_enabled: enabled })
});
if (!resp.ok) {
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 || 'Failed to update registration setting');
}
} catch (e) {
document.getElementById('ds-registration').checked = !enabled;
if (!enabled) showElement('registration-warning', 'flex');
else hideElement('registration-warning');
alert('Error: ' + e.message);
}
}
document.getElementById('oidc-enabled').addEventListener('change', function() {
if (this.checked) showElement('oidc-form');
else hideElement('oidc-form');
});
document.getElementById('disable-password').addEventListener('change', function() {
if (this.checked) showElement('password-warning', 'flex');
else hideElement('password-warning');
});
function showOidcStatus(msg, type) {
const el = document.getElementById('oidc-status');
el.textContent = msg;
el.className = 'alert alert-' + type;
}
function copyCallback() {
const text = document.getElementById('callback-url').textContent;
navigator.clipboard.writeText(text);
}
async function testConnection() {
const url = document.getElementById('issuer-url').value.trim();
if (!url) { showOidcStatus('Enter an Issuer URL first', 'error'); return; }
const btn = document.getElementById('discover-btn');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Discovering…';
const resultDiv = document.getElementById('discovery-result');
try {
const resp = await fetch(API + '/admin/settings/oidc/test', { method: 'POST', headers: headers(), body: JSON.stringify({ issuer_url: url }) });
const r = await resp.json();
if (r.success) {
resultDiv.innerHTML = '<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ' + r.message + '</strong><dl><dt>Issuer</dt><dd>' + (r.issuer||'—') + '</dd><dt>Auth Endpoint</dt><dd>' + (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;
} else {
resultDiv.innerHTML = '<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ' + r.message + '</strong></div>';
}
} catch (e) { resultDiv.innerHTML = '<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ' + e.message + '</div>'; }
btn.disabled = false; btn.innerHTML = '<i class="fas fa-search"></i> Auto-discover';
}
async function saveOidcSettings() {
const btn = document.getElementById('save-btn');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 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,
};
try {
const resp = await fetch(API + '/admin/settings/oidc', { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
if (resp.ok) { showOidcStatus('Settings saved — OIDC is now ' + (body.enabled ? 'active' : 'disabled'), 'success'); loadDashboard(); }
else { const e = await resp.json().catch(()=>({})); showOidcStatus('Error: ' + (e.message || resp.statusText), 'error'); }
} catch (e) { showOidcStatus('Network error: ' + e.message, 'error'); }
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> Save';
}
async function init() {
if (!token) { showAccessDenied(); return; }
try {
const me = await fetch(API + '/auth/me', { headers: headers() });
if (!me.ok) { showAccessDenied(); return; }
const user = await me.json();
if (user.role !== 'admin') { showAccessDenied(); return; }
currentAdminId = user.id;
const oidcResp = await fetch(API + '/admin/settings/oidc', { headers: headers() });
if (oidcResp.ok) {
const s = await oidcResp.json();
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;
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 => {
const badge = document.getElementById('badge-' + field);
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
});
}
await loadDashboard();
hideElement('loading');
showElement('main-content');
} catch (e) { console.error(e); showAccessDenied(); }
}
function showAccessDenied() {
hideElement('loading');
showElement('access-denied');
}
init();
+138
View File
@@ -0,0 +1,138 @@
const API = '/api';
const token = localStorage.getItem('oxicloud_token') || localStorage.getItem('token') || localStorage.getItem('access_token');
function headers() {
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function timeAgo(dateStr) {
if (!dateStr) return 'Never';
const d = new Date(dateStr);
const now = new Date();
const secs = Math.floor((now - d) / 1000);
if (secs < 60) return 'Just now';
if (secs < 3600) return Math.floor(secs/60) + ' min ago';
if (secs < 86400) return Math.floor(secs/3600) + 'h ago';
if (secs < 2592000) return Math.floor(secs/86400) + ' days ago';
return d.toLocaleDateString();
}
async function init() {
if (!token) { showError(); return; }
try {
const resp = await fetch(API + '/auth/me', { headers: headers() });
if (!resp.ok) { showError(); return; }
const user = await resp.json();
const initials = (user.username || '?').substring(0, 2).toUpperCase();
document.getElementById('p-avatar').textContent = initials;
document.getElementById('p-username').textContent = user.username;
document.getElementById('p-email').textContent = user.email || '';
const badge = document.getElementById('p-role-badge');
if (user.role === 'admin') {
badge.className = 'role-badge role-badge-admin';
badge.innerHTML = '<i class="fas fa-shield-alt"></i> Administrator';
} else {
badge.className = 'role-badge role-badge-user';
badge.innerHTML = '<i class="fas fa-user"></i> User';
}
document.getElementById('p-detail-username').textContent = user.username;
document.getElementById('p-detail-email').textContent = user.email || '—';
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? 'Administrator' : 'User';
document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
const used = user.storage_used_bytes || 0;
const quota = user.storage_quota_bytes || 0;
const pct = quota > 0 ? Math.min(Math.round((used / quota) * 100), 100) : 0;
document.getElementById('p-storage-used').textContent = formatBytes(used);
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
document.getElementById('p-storage-pct').textContent = quota > 0 ? pct + '%' : '—';
const bar = document.getElementById('p-storage-bar');
bar.style.width = pct + '%';
bar.className = 'storage-fill ' + (pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green');
document.getElementById('p-storage-text').textContent = formatBytes(used) + ' / ' + (quota > 0 ? formatBytes(quota) : 'Unlimited');
if (user.auth_provider && user.auth_provider !== 'local') {
document.getElementById('password-section').style.display = 'none';
}
try {
const oidcResp = await fetch(API + '/auth/oidc/providers');
if (oidcResp.ok) {
const oidcInfo = await oidcResp.json();
if (!oidcInfo.password_login_enabled) {
document.getElementById('password-section').style.display = 'none';
}
}
} catch (oidcErr) {
}
document.getElementById('loading').style.display = 'none';
document.getElementById('main-content').style.display = 'block';
} catch (e) {
console.error(e);
showError();
}
}
function showError() {
document.getElementById('loading').style.display = 'none';
document.getElementById('auth-error').style.display = 'block';
}
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 statusEl = document.getElementById('pw-status');
if (newPw !== confirmPw) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> Passwords do not match</div>';
return false;
}
if (newPw.length < 8) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> Password must be at least 8 characters</div>';
return false;
}
const btn = document.getElementById('pw-submit');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating…';
try {
const resp = await fetch(API + '/auth/change-password', {
method: 'PUT',
headers: headers(),
body: JSON.stringify({ current_password: currentPw, new_password: newPw })
});
if (resp.ok) {
statusEl.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> Password updated successfully</div>';
document.getElementById('password-form').reset();
} else {
const err = await resp.json().catch(() => ({}));
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + (err.message || 'Failed to change password') + '</div>';
}
} catch (err) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> Network error: ' + err.message + '</div>';
}
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save"></i> Update Password';
return false;
}
init();
+406
View File
@@ -0,0 +1,406 @@
/**
* OxiCloud - Shared Resources Page (/shared)
* All operations go through the backend API at /api/shares.
*/
// Authentication check
function checkAuthentication() {
const token = localStorage.getItem('oxicloud_token');
const tokenExpiry = localStorage.getItem('oxicloud_token_expiry');
if (!token || !tokenExpiry || new Date(tokenExpiry) < new Date()) {
window.location.href = '/login';
}
}
document.addEventListener('DOMContentLoaded', async () => {
// ── i18n ── (translatePage is called automatically by i18n.js on init)
function t(key, fallback) {
return (window.i18n && window.i18n.t) ? window.i18n.t(key, fallback) : fallback;
}
// ── Auth headers helper ──
function authHeaders(json = false) {
const h = {};
const token = localStorage.getItem('oxicloud_token');
if (token) h['Authorization'] = `Bearer ${token}`;
if (json) h['Content-Type'] = 'application/json';
return h;
}
// ── Elements ──
const sharedItemsList = document.getElementById('shared-items-list');
const emptySharedState = document.getElementById('empty-shared-state');
const filterType = document.getElementById('filter-type');
const sortBy = document.getElementById('sort-by');
const sharedSearch = document.getElementById('shared-search');
const sharedSearchBtn = document.getElementById('shared-search-btn');
const goToFilesBtn = document.getElementById('go-to-files');
// Share dialog elements
const shareDialog = document.getElementById('share-dialog');
const shareDialogCloseBtn = shareDialog ? shareDialog.querySelector('.close-dialog-btn') : null;
const shareDialogIcon = document.getElementById('share-dialog-icon');
const shareDialogName = document.getElementById('share-dialog-name');
const shareLinkUrl = document.getElementById('share-link-url');
const copyLinkBtn = document.getElementById('copy-link-btn');
const enablePassword = document.getElementById('enable-password');
const sharePassword = document.getElementById('share-password');
const generatePasswordBtn = document.getElementById('generate-password');
const enableExpiration = document.getElementById('enable-expiration');
const shareExpiration = document.getElementById('share-expiration');
const permissionRead = document.getElementById('permission-read');
const permissionWrite = document.getElementById('permission-write');
const permissionReshare = document.getElementById('permission-reshare');
const updateShareBtn = document.getElementById('update-share-btn');
const removeShareBtn = document.getElementById('remove-share-btn');
// Notification dialog elements
const notificationDialog = document.getElementById('share-notification-dialog');
const notificationCloseBtn = notificationDialog ? notificationDialog.querySelector('.close-dialog-btn') : null;
const notifyDialogIcon = document.getElementById('notify-dialog-icon');
const notifyDialogName = document.getElementById('notify-dialog-name');
const notificationEmail = document.getElementById('notification-email');
const notificationMessage = document.getElementById('notification-message');
const sendNotificationBtn = document.getElementById('send-notification-btn');
// Notification banner
const notificationBanner = document.getElementById('notification-banner');
const notificationBannerMessage = document.getElementById('notification-message');
const closeNotificationBtn = document.getElementById('close-notification');
// ── State ──
let currentSharedItem = null;
let allSharedItems = [];
let filteredItems = [];
// ── Init ──
checkAuthentication();
await loadSharedItems();
// ── Event listeners ──
if (filterType) filterType.addEventListener('change', filterAndSortItems);
if (sortBy) sortBy.addEventListener('change', filterAndSortItems);
if (sharedSearchBtn) sharedSearchBtn.addEventListener('click', filterAndSortItems);
if (sharedSearch) {
let searchDebounce;
sharedSearch.addEventListener('input', () => {
clearTimeout(searchDebounce);
searchDebounce = setTimeout(filterAndSortItems, 250);
});
sharedSearch.addEventListener('keyup', e => { if (e.key === 'Enter') { clearTimeout(searchDebounce); filterAndSortItems(); } });
}
if (goToFilesBtn) goToFilesBtn.addEventListener('click', () => window.location.href = '/');
if (shareDialogCloseBtn) shareDialogCloseBtn.addEventListener('click', closeShareDialog);
if (copyLinkBtn) copyLinkBtn.addEventListener('click', copyShareLink);
if (enablePassword) enablePassword.addEventListener('change', () => {
if (sharePassword) { sharePassword.disabled = !enablePassword.checked; if (enablePassword.checked) sharePassword.focus(); }
});
if (generatePasswordBtn) generatePasswordBtn.addEventListener('click', generatePassword);
if (enableExpiration) enableExpiration.addEventListener('change', () => {
if (shareExpiration) { shareExpiration.disabled = !enableExpiration.checked; if (enableExpiration.checked) shareExpiration.focus(); }
});
if (updateShareBtn) updateShareBtn.addEventListener('click', updateSharedItem);
if (removeShareBtn) removeShareBtn.addEventListener('click', removeSharedItem);
if (notificationCloseBtn) notificationCloseBtn.addEventListener('click', closeNotificationDialog);
if (sendNotificationBtn) sendNotificationBtn.addEventListener('click', sendNotification);
if (closeNotificationBtn) closeNotificationBtn.addEventListener('click', () => {
if (notificationBanner) notificationBanner.classList.remove('active');
});
// ── Load shares from backend ──
async function loadSharedItems() {
try {
const res = await fetch('/api/shares?page=1&per_page=1000', {
headers: authHeaders()
});
if (res.ok) {
const data = await res.json();
allSharedItems = data.items || [];
} else {
allSharedItems = [];
}
} catch (err) {
console.error('Error loading shared items:', err);
allSharedItems = [];
}
filterAndSortItems();
}
// ── Filter & sort ──
function filterAndSortItems() {
const type = filterType ? filterType.value : 'all';
const sort = sortBy ? sortBy.value : 'date';
const searchTerm = sharedSearch ? sharedSearch.value.toLowerCase() : '';
filteredItems = allSharedItems.filter(item => {
if (type !== 'all' && item.item_type !== type) return false;
const name = (item.item_name || item.item_id || '').toLowerCase();
return name.includes(searchTerm);
});
filteredItems.sort((a, b) => {
if (sort === 'name') {
return (a.item_name || a.item_id || '').localeCompare(b.item_name || b.item_id || '');
} else if (sort === 'date') {
return (b.created_at || 0) - (a.created_at || 0);
} else if (sort === 'expiration') {
if (!a.expires_at && !b.expires_at) return 0;
if (!a.expires_at) return 1;
if (!b.expires_at) return -1;
return a.expires_at - b.expires_at;
}
return 0;
});
displaySharedItems();
}
// ── Display table ──
function displaySharedItems() {
if (!sharedItemsList) return;
sharedItemsList.innerHTML = '';
if (filteredItems.length === 0) {
if (emptySharedState) emptySharedState.style.display = 'flex';
const listContainer = document.querySelector('.shared-list-container');
if (listContainer) listContainer.style.display = 'none';
return;
}
if (emptySharedState) emptySharedState.style.display = 'none';
const listContainer = document.querySelector('.shared-list-container');
if (listContainer) listContainer.style.display = 'block';
filteredItems.forEach(item => {
const row = document.createElement('tr');
const displayName = item.item_name || item.item_id || 'Unknown';
// Name
const nameCell = document.createElement('td');
nameCell.className = 'shared-item-name';
nameCell.innerHTML = `<span class="item-icon">${item.item_type === 'file' ? '📄' : '📁'}</span><span>${displayName}</span>`;
// Type
const typeCell = document.createElement('td');
typeCell.textContent = item.item_type === 'file' ? t('shared_typeFile', 'File') : t('shared_typeFolder', 'Folder');
// Date
const dateCell = document.createElement('td');
dateCell.textContent = formatDate(item.created_at);
// Expiration
const expirationCell = document.createElement('td');
expirationCell.textContent = item.expires_at ? formatDate(item.expires_at) : t('shared_noExpiration', 'No expiration');
// Permissions
const permissionsCell = document.createElement('td');
const perms = [];
if (item.permissions?.read) perms.push(t('share_permissionRead', 'Read'));
if (item.permissions?.write) perms.push(t('share_permissionWrite', 'Write'));
if (item.permissions?.reshare) perms.push(t('share_permissionReshare', 'Reshare'));
permissionsCell.textContent = perms.join(', ') || 'Read';
// Password
const passwordCell = document.createElement('td');
passwordCell.textContent = item.has_password ? t('shared_hasPassword', 'Yes') : t('shared_noPassword', 'No');
// Actions
const actionsCell = document.createElement('td');
actionsCell.className = 'shared-item-actions';
const editBtn = document.createElement('button');
editBtn.className = 'action-btn edit-btn';
editBtn.innerHTML = '<span class="action-icon">✏️</span>';
editBtn.title = t('shared_editShare', 'Edit Share');
editBtn.addEventListener('click', () => openShareDialog(item));
const notifyBtn = document.createElement('button');
notifyBtn.className = 'action-btn notify-btn';
notifyBtn.innerHTML = '<span class="action-icon">📧</span>';
notifyBtn.title = t('shared_notifyShare', 'Notify Someone');
notifyBtn.addEventListener('click', () => openNotificationDialog(item));
const cpBtn = document.createElement('button');
cpBtn.className = 'action-btn copy-btn';
cpBtn.innerHTML = '<span class="action-icon">📋</span>';
cpBtn.title = t('shared_copyLink', 'Copy Link');
cpBtn.addEventListener('click', () => {
navigator.clipboard.writeText(item.url)
.then(() => showNotification(t('shared_linkCopied', 'Link copied!')))
.catch(() => showNotification(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 = t('shared_removeShare', 'Remove Share');
rmBtn.addEventListener('click', () => { currentSharedItem = item; removeSharedItem(); });
actionsCell.append(editBtn, notifyBtn, cpBtn, rmBtn);
row.append(nameCell, typeCell, dateCell, expirationCell, permissionsCell, passwordCell, actionsCell);
sharedItemsList.appendChild(row);
});
}
// ── Share dialog ──
function openShareDialog(item) {
currentSharedItem = item;
const dn = item.item_name || item.item_id || 'Unknown';
if (shareDialogIcon) shareDialogIcon.textContent = item.item_type === 'file' ? '📄' : '📁';
if (shareDialogName) shareDialogName.textContent = dn;
if (shareLinkUrl) shareLinkUrl.value = item.url || '';
if (permissionRead) permissionRead.checked = item.permissions?.read !== false;
if (permissionWrite) permissionWrite.checked = !!item.permissions?.write;
if (permissionReshare) permissionReshare.checked = !!item.permissions?.reshare;
if (enablePassword) {
enablePassword.checked = item.has_password;
if (sharePassword) { sharePassword.disabled = !enablePassword.checked; sharePassword.value = ''; }
}
if (enableExpiration) {
enableExpiration.checked = !!item.expires_at;
if (shareExpiration) {
shareExpiration.disabled = !enableExpiration.checked;
shareExpiration.value = item.expires_at ? new Date(item.expires_at * 1000).toISOString().split('T')[0] : '';
}
}
if (shareDialog) shareDialog.classList.add('active');
}
function closeShareDialog() {
if (shareDialog) shareDialog.classList.remove('active');
currentSharedItem = null;
}
// ── Notification dialog ──
function openNotificationDialog(item) {
currentSharedItem = item;
const dn = item.item_name || item.item_id || 'Unknown';
if (notifyDialogIcon) notifyDialogIcon.textContent = item.item_type === 'file' ? '📄' : '📁';
if (notifyDialogName) notifyDialogName.textContent = dn;
if (notificationEmail) notificationEmail.value = '';
if (notificationMessage) notificationMessage.value = '';
if (notificationDialog) notificationDialog.classList.add('active');
}
function closeNotificationDialog() {
if (notificationDialog) notificationDialog.classList.remove('active');
currentSharedItem = null;
}
function copyShareLink() {
if (!shareLinkUrl) return;
navigator.clipboard.writeText(shareLinkUrl.value)
.then(() => showNotification(t('shared_linkCopied', 'Link copied!')))
.catch(() => showNotification(t('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
}
// ── Generate secure password with crypto API ──
function generatePassword() {
if (!sharePassword || !enablePassword) return;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
const array = new Uint32Array(16);
crypto.getRandomValues(array);
let password = '';
for (let i = 0; i < 16; i++) {
password += chars[array[i] % chars.length];
}
sharePassword.value = password;
enablePassword.checked = true;
sharePassword.disabled = false;
}
// ── Update share via API ──
async function updateSharedItem() {
if (!currentSharedItem) return;
const body = {
permissions: {
read: permissionRead ? permissionRead.checked : true,
write: permissionWrite ? permissionWrite.checked : false,
reshare: permissionReshare ? permissionReshare.checked : false
},
password: (enablePassword && enablePassword.checked && sharePassword && sharePassword.value) ? sharePassword.value : null,
expires_at: (enableExpiration && enableExpiration.checked && shareExpiration && shareExpiration.value)
? Math.floor(new Date(shareExpiration.value).getTime() / 1000)
: null
};
try {
const res = await fetch(`/api/shares/${currentSharedItem.id}`, {
method: 'PUT',
headers: authHeaders(true),
body: JSON.stringify(body)
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `Server error ${res.status}`);
}
showNotification(t('shared_itemUpdated', 'Share settings updated'));
} catch (err) {
console.error('Error updating share:', err);
showNotification(err.message || 'Error updating share', 'error');
}
closeShareDialog();
await loadSharedItems();
}
// ── Remove share via API ──
async function removeSharedItem() {
if (!currentSharedItem) return;
try {
const res = await fetch(`/api/shares/${currentSharedItem.id}`, {
method: 'DELETE',
headers: authHeaders()
});
if (!res.ok && res.status !== 204) throw new Error(`Server error ${res.status}`);
showNotification(t('shared_itemRemoved', 'Share removed'));
} catch (err) {
console.error('Error removing share:', err);
showNotification('Error removing share', 'error');
}
closeShareDialog();
await loadSharedItems();
}
// ── Send notification (stub) ──
function sendNotification() {
if (!currentSharedItem) return;
const email = notificationEmail ? notificationEmail.value.trim() : '';
const message = notificationMessage ? notificationMessage.value.trim() : '';
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showNotification(t('shared_invalidEmail', 'Please enter a valid email address'), 'error');
return;
}
if (window.fileSharing && window.fileSharing.sendShareNotification) {
window.fileSharing.sendShareNotification(currentSharedItem.url, email, message)
.then(() => { closeNotificationDialog(); showNotification(t('shared_notificationSent', 'Notification sent')); })
.catch(() => showNotification(t('shared_notificationFailed', 'Failed to send notification'), 'error'));
}
}
// ── Helpers ──
function showNotification(message, type = 'success') {
if (notificationBannerMessage && notificationBanner) {
notificationBannerMessage.textContent = message;
notificationBanner.className = 'notification-banner active ' + type;
setTimeout(() => notificationBanner.classList.remove('active'), 5000);
} else if (window.ui && window.ui.showNotification) {
window.ui.showNotification(message, type);
} else {
alert(message);
}
}
function formatDate(value) {
return window.formatDateShort ? window.formatDateShort(value) : String(value);
}
});
+556
View File
@@ -0,0 +1,556 @@
/**
* OxiCloud - Shared View Component
* In-app shared files view. All operations go through the backend API.
*/
const sharedView = {
// State
items: [],
filteredItems: [],
currentItem: null,
/** Auth header helper */
_headers(json = false) {
const h = {};
const token = localStorage.getItem('oxicloud_token');
if (token) h['Authorization'] = `Bearer ${token}`;
if (json) h['Content-Type'] = 'application/json';
return h;
},
init() {
console.log('Initializing shared view component (API-backed)');
this.loadItems();
},
show() {
this.displayUI();
this.attachEventListeners();
this.loadItems().then(() => this.filterAndSortItems());
},
hide() {
const c = document.getElementById('shared-container');
if (c) c.style.display = 'none';
},
// Load shared items from backend API
async loadItems() {
try {
const res = await fetch('/api/shares?page=1&per_page=1000', {
headers: this._headers()
});
if (res.ok) {
const data = await res.json();
this.items = data.items || [];
} else {
this.items = [];
}
} catch (err) {
console.error('Error loading shared items:', err);
this.items = [];
}
this.filteredItems = [...this.items];
},
// Create and display the shared view UI
displayUI() {
const contentArea = document.querySelector('.content-area');
let container = document.getElementById('shared-container');
if (!container) {
container = document.createElement('div');
container.id = 'shared-container';
container.className = 'shared-view-container';
if (contentArea) contentArea.appendChild(container);
}
container.style.display = 'block';
container.innerHTML = `
<div class="shared-header">
<div class="shared-filters">
<select id="filter-type" class="shared-filter-select">
<option value="all" data-i18n="shared_filterAll">All</option>
<option value="file" data-i18n="shared_filterFiles">Files</option>
<option value="folder" data-i18n="shared_filterFolders">Folders</option>
</select>
<select id="sort-by" class="shared-filter-select">
<option value="date" data-i18n="shared_sortByDate">Sort by date</option>
<option value="name" data-i18n="shared_sortByName">Sort by name</option>
<option value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</option>
</select>
</div>
</div>
<div id="empty-shared-state" class="empty-state" style="display:none;">
<div class="empty-state-icon">📤</div>
<h3 data-i18n="shared_emptyStateTitle">No shared items</h3>
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
<button id="empty-go-to-files" class="button primary" data-i18n="shared_goToFiles">Go to Files</button>
</div>
<div class="shared-list-container" style="display:none;">
<table class="shared-table">
<thead>
<tr>
<th data-i18n="shared_colName">Name</th>
<th data-i18n="shared_colType">Type</th>
<th data-i18n="shared_colDateShared">Date</th>
<th data-i18n="shared_colExpiration">Expiration</th>
<th data-i18n="shared_colPermissions">Permissions</th>
<th data-i18n="shared_colPassword">Password</th>
<th data-i18n="shared_colActions">Actions</th>
</tr>
</thead>
<tbody id="shared-items-list"></tbody>
</table>
</div>
<!-- Share Edit Dialog (sharedView-specific) -->
<div id="shared-view-edit-dialog" class="shared-dialog">
<div class="shared-dialog-content">
<div class="shared-dialog-header">
<span id="sv-dialog-icon">📄</span>
<span id="sv-dialog-name">Item</span>
<button class="close-dialog-btn">&times;</button>
</div>
<div class="share-link-section">
<label data-i18n="share.linkLabel">Share Link:</label>
<div class="share-link-input">
<input type="text" id="sv-share-link-url" readonly>
<button id="sv-copy-link-btn" class="button" data-i18n="share.copyLink">Copy</button>
</div>
</div>
<div class="share-permissions-section">
<h4 data-i18n="share.permissions">Permissions</h4>
<label><input type="checkbox" id="sv-permission-read" checked> <span data-i18n="share.permissionRead">Read</span></label>
<label><input type="checkbox" id="sv-permission-write"> <span data-i18n="share.permissionWrite">Write</span></label>
<label><input type="checkbox" id="sv-permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
</div>
<div class="share-password-section">
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.enablePassword">Password protection</span></label>
<div class="password-input-group">
<input type="text" id="sv-share-password" disabled placeholder="Enter password">
<button id="sv-generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
</div>
</div>
<div class="share-expiration-section">
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.enableExpiration">Set expiration</span></label>
<input type="date" id="sv-share-expiration" disabled>
</div>
<div class="share-actions">
<button id="sv-update-share-btn" class="button primary" data-i18n="share.update">Update</button>
<button id="sv-remove-share-btn" class="button danger" data-i18n="share.remove">Remove Share</button>
</div>
</div>
</div>
<!-- Notification Dialog (sharedView-specific) -->
<div id="sv-notification-dialog" class="shared-dialog">
<div class="shared-dialog-content">
<div class="shared-dialog-header">
<span id="sv-notify-dialog-icon">📧</span>
<span id="sv-notify-dialog-name">Item</span>
<button class="close-dialog-btn">&times;</button>
</div>
<div class="notification-form">
<div class="form-group">
<label data-i18n="share.notifyEmail">Email:</label>
<input type="email" id="sv-notification-email" placeholder="recipient@example.com">
</div>
<div class="form-group">
<label data-i18n="share.notifyMessage">Message (optional):</label>
<textarea id="sv-notification-message" rows="3"></textarea>
</div>
</div>
<div class="notification-actions">
<button id="sv-send-notification-btn" class="button primary" data-i18n="share.notifySend">Send Notification</button>
</div>
</div>
</div>
`;
// Hide other UI elements
const filesGrid = document.getElementById('files-grid');
const filesListView = document.getElementById('files-list-view');
if (filesGrid) filesGrid.style.display = 'none';
if (filesListView) filesListView.style.display = 'none';
if (window.i18n && window.i18n.translateElement) {
window.i18n.translateElement(container);
}
},
// Attach event listeners
attachEventListeners() {
const filterType = document.getElementById('filter-type');
const sortBy = document.getElementById('sort-by');
const emptyGoToFiles = document.getElementById('empty-go-to-files');
if (filterType) filterType.addEventListener('change', () => this.filterAndSortItems());
if (sortBy) sortBy.addEventListener('change', () => this.filterAndSortItems());
if (emptyGoToFiles) emptyGoToFiles.addEventListener('click', () => window.switchToFilesView());
// Share dialog (sharedView-specific IDs)
const shareDialog = document.getElementById('shared-view-edit-dialog');
if (shareDialog) {
const closeBtn = shareDialog.querySelector('.close-dialog-btn');
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');
if (enablePw) enablePw.addEventListener('change', () => {
if (pwField) { pwField.disabled = !enablePw.checked; if (enablePw.checked) pwField.focus(); }
});
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');
if (enableExp) enableExp.addEventListener('change', () => {
if (expField) { expField.disabled = !enableExp.checked; if (enableExp.checked) expField.focus(); }
});
const updateBtn = document.getElementById('sv-update-share-btn');
if (updateBtn) updateBtn.addEventListener('click', () => this.updateSharedItem());
const removeBtn = document.getElementById('sv-remove-share-btn');
if (removeBtn) removeBtn.addEventListener('click', () => this.removeSharedItem());
}
// Notification dialog (sharedView-specific IDs)
const notifDialog = document.getElementById('sv-notification-dialog');
if (notifDialog) {
const closeBtn = notifDialog.querySelector('.close-dialog-btn');
if (closeBtn) closeBtn.addEventListener('click', () => this.closeNotificationDialog());
const sendBtn = document.getElementById('sv-send-notification-btn');
if (sendBtn) sendBtn.addEventListener('click', () => this.sendNotification());
}
},
// Filter and sort items
filterAndSortItems() {
const filterType = document.getElementById('filter-type');
const sortBy = document.getElementById('sort-by');
const type = filterType ? filterType.value : 'all';
const sort = sortBy ? sortBy.value : 'date';
// Use the top-bar search if available, otherwise no filter
const topSearch = document.getElementById('shared-search');
const searchTerm = topSearch ? topSearch.value.toLowerCase() : '';
this.filteredItems = this.items.filter(item => {
if (type !== 'all' && item.item_type !== type) return false;
const name = (item.item_name || item.item_id || '').toLowerCase();
return name.includes(searchTerm);
});
this.filteredItems.sort((a, b) => {
if (sort === 'name') {
return (a.item_name || a.item_id || '').localeCompare(b.item_name || b.item_id || '');
} else if (sort === 'date') {
return (b.created_at || 0) - (a.created_at || 0);
} else if (sort === 'expiration') {
if (!a.expires_at && !b.expires_at) return 0;
if (!a.expires_at) return 1;
if (!b.expires_at) return -1;
return a.expires_at - b.expires_at;
}
return 0;
});
this.displaySharedItems();
},
// Display items in the table
displaySharedItems() {
const sharedItemsList = document.getElementById('shared-items-list');
const emptyState = document.getElementById('empty-shared-state');
const listContainer = document.querySelector('.shared-list-container');
if (!sharedItemsList || !emptyState || !listContainer) return;
sharedItemsList.innerHTML = '';
if (this.filteredItems.length === 0) {
emptyState.style.display = 'flex';
listContainer.style.display = 'none';
return;
}
emptyState.style.display = 'none';
listContainer.style.display = 'block';
this.filteredItems.forEach(item => {
const row = document.createElement('tr');
const displayName = item.item_name || item.item_id || 'Unknown';
const nameCell = document.createElement('td');
nameCell.className = 'shared-item-name';
const iconSpan = document.createElement('span');
iconSpan.className = 'item-icon';
iconSpan.textContent = item.item_type === 'file' ? '📄' : '📁';
const nameSpan = document.createElement('span');
nameSpan.textContent = displayName;
nameCell.appendChild(iconSpan);
nameCell.appendChild(nameSpan);
const typeCell = document.createElement('td');
typeCell.textContent = item.item_type === 'file' ? this.translate('shared_typeFile', 'File') : this.translate('shared_typeFolder', 'Folder');
const dateCell = document.createElement('td');
dateCell.textContent = this.formatDate(item.created_at);
const expCell = document.createElement('td');
expCell.textContent = item.expires_at ? this.formatDate(item.expires_at) : this.translate('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'));
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');
const actionsCell = document.createElement('td');
actionsCell.className = 'shared-item-actions';
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.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.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.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'));
});
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.addEventListener('click', () => { this.currentItem = item; this.removeSharedItem(); });
actionsCell.append(editBtn, notifyBtn, copyBtn, rmBtn);
row.append(nameCell, typeCell, dateCell, expCell, permCell, pwCell, actionsCell);
sharedItemsList.appendChild(row);
});
},
// Open share dialog
openShareDialog(item) {
this.currentItem = item;
const shareDialog = document.getElementById('shared-view-edit-dialog');
const dn = item.item_name || item.item_id || 'Unknown';
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');
if (!shareDialog) return;
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
if (nameEl) nameEl.textContent = dn;
if (urlEl) urlEl.value = item.url || '';
if (permRead) permRead.checked = item.permissions?.read !== false;
if (permWrite) permWrite.checked = !!item.permissions?.write;
if (permReshare) permReshare.checked = !!item.permissions?.reshare;
if (enablePw) {
enablePw.checked = item.has_password;
if (pwField) { pwField.disabled = !enablePw.checked; pwField.value = ''; }
}
if (enableExp) {
enableExp.checked = !!item.expires_at;
if (expField) {
expField.disabled = !enableExp.checked;
expField.value = item.expires_at ? new Date(item.expires_at * 1000).toISOString().split('T')[0] : '';
}
}
shareDialog.classList.add('active');
},
closeShareDialog() {
const d = document.getElementById('shared-view-edit-dialog');
if (d) d.classList.remove('active');
this.currentItem = null;
},
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');
if (!d) return;
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
if (nameEl) nameEl.textContent = dn;
if (emailEl) emailEl.value = '';
if (msgEl) msgEl.value = '';
d.classList.add('active');
},
closeNotificationDialog() {
const d = document.getElementById('sv-notification-dialog');
if (d) d.classList.remove('active');
this.currentItem = null;
},
copyShareLink() {
const el = 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'));
},
// Generate secure password with crypto API
generatePassword() {
const pwField = document.getElementById('sv-share-password');
const enablePw = document.getElementById('sv-enable-password');
if (!pwField || !enablePw) return;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
const array = new Uint32Array(16);
crypto.getRandomValues(array);
let password = '';
for (let i = 0; i < 16; i++) {
password += chars[array[i] % chars.length];
}
pwField.value = password;
enablePw.checked = true;
pwField.disabled = false;
},
// Update share via API
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 body = {
permissions: {
read: permRead ? permRead.checked : true,
write: permWrite ? permWrite.checked : false,
reshare: permReshare ? permReshare.checked : false
},
password: (enablePw && enablePw.checked && pwField && pwField.value) ? pwField.value : null,
expires_at: (enableExp && enableExp.checked && expField && expField.value)
? Math.floor(new Date(expField.value).getTime() / 1000)
: null
};
try {
const res = await fetch(`/api/shares/${this.currentItem.id}`, {
method: 'PUT',
headers: this._headers(true),
body: JSON.stringify(body)
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `Server error ${res.status}`);
}
this.showNotification(this.translate('shared_itemUpdated', 'Share settings updated'));
} catch (err) {
console.error('Error updating share:', err);
this.showNotification(err.message || 'Error updating share', 'error');
}
this.closeShareDialog();
await this.loadItems();
this.filterAndSortItems();
},
// Remove share via API
async removeSharedItem() {
if (!this.currentItem) return;
try {
const res = await fetch(`/api/shares/${this.currentItem.id}`, {
method: 'DELETE',
headers: this._headers()
});
if (!res.ok && res.status !== 204) throw new Error(`Server error ${res.status}`);
this.showNotification(this.translate('shared_itemRemoved', 'Share removed'));
} catch (err) {
console.error('Error removing share:', err);
this.showNotification('Error removing share', 'error');
}
this.closeShareDialog();
await this.loadItems();
this.filterAndSortItems();
},
// Send notification (stub)
sendNotification() {
if (!this.currentItem) return;
const emailEl = document.getElementById('sv-notification-email');
const msgEl = 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');
return;
}
if (window.fileSharing && window.fileSharing.sendShareNotification) {
window.fileSharing.sendShareNotification(this.currentItem.url, email, message)
.then(() => { this.closeNotificationDialog(); this.showNotification(this.translate('shared_notificationSent', 'Notification sent')); })
.catch(() => this.showNotification(this.translate('shared_notificationFailed', 'Failed to send notification'), 'error'));
}
},
showNotification(message, type = 'success') {
if (window.ui && window.ui.showNotification) {
window.ui.showNotification(message, type);
} else {
alert(message);
}
},
validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
},
formatDate(value) {
return window.formatDateShort ? window.formatDateShort(value) : String(value);
},
translate(key, defaultText) {
if (window.i18n && window.i18n.t) return window.i18n.t(key, defaultText);
return defaultText;
}
};
window.sharedView = sharedView;