fix(csp): remove all inline styles, scripts, and event handlers for strict CSP compliance
Replace ~50 inline style="" attributes with CSS classes, move 3 inline
<script> blocks to external JS files, replace all inline event handlers
(onclick, onerror) with addEventListener, and remove createElement('style')
from icons.js. All changes support the strict CSP policy (style-src 'self';
script-src 'self') without weakening it.
This commit is contained in:
@@ -124,17 +124,35 @@ async function loadUsers() {
|
||||
'<td><span class="badge badge-' + escapeHtml(u.role) + '">' + (u.role === 'admin' ? '<i class="fas fa-shield-alt badge-admin-icon-small"></i> ' : '') + escapeHtml(u.role) + '</span></td>' +
|
||||
'<td>' + authBadge + '</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><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + quotaColor + '" data-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(\'' + _escJs(u.id) + '\',\'' + _escJs(u.username) + '\',' + u.storage_quota_bytes + ')" title="Edit quota"><i class="fas fa-box"></i></button>' +
|
||||
(isOidc ? '' : '<button class="btn btn-sm btn-secondary" onclick="openResetPasswordModal(\'' + _escJs(u.id) + '\',\'' + _escJs(u.username) + '\')" title="Reset password"><i class="fas fa-key"></i></button>') +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="toggleRole(\'' + _escJs(u.id) + '\',\'' + _escJs(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(\'' + _escJs(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(\'' + _escJs(u.id) + '\',\'' + _escJs(u.username) + '\')" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
|
||||
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="quota" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" data-quota="' + u.storage_quota_bytes + '" title="Edit quota"><i class="fas fa-box"></i></button>' +
|
||||
(isOidc ? '' : '<button class="btn btn-sm btn-secondary admin-action-btn" data-action="reset-pw" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" title="Reset password"><i class="fas fa-key"></i></button>') +
|
||||
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' + _escJs(u.id) + '" data-role="' + _escJs(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') + ' admin-action-btn" data-action="toggle-active" data-uid="' + _escJs(u.id) + '" data-active="' + 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 admin-action-btn" data-action="delete" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
|
||||
'</div></td></tr>';
|
||||
}).join('');
|
||||
|
||||
// Set dynamic progress bar widths (CSP-safe via JS property)
|
||||
document.querySelectorAll('.progress-fill[data-width]').forEach(function(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(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var 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-active') toggleActive(btn.dataset.uid, btn.dataset.active === 'true');
|
||||
else if (action === 'delete') deleteUser(btn.dataset.uid, btn.dataset.uname);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Read error type from URL query parameter
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var errorType = params.get('type') || 'generic';
|
||||
|
||||
var errorTitle = document.getElementById('error-title');
|
||||
var errorMessage = document.getElementById('error-message');
|
||||
var errorAction = document.getElementById('error-action');
|
||||
|
||||
switch(errorType) {
|
||||
case 'invalid-credentials':
|
||||
errorTitle.textContent = 'Login Failed';
|
||||
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
|
||||
errorAction.textContent = 'Try Again';
|
||||
errorAction.addEventListener('click', function() { history.back(); });
|
||||
break;
|
||||
case 'session-expired':
|
||||
errorTitle.textContent = 'Session Expired';
|
||||
errorMessage.textContent = 'Your session has expired. Please try again.';
|
||||
errorAction.textContent = 'Close Window';
|
||||
errorAction.addEventListener('click', function() { window.close(); });
|
||||
break;
|
||||
case 'not-found':
|
||||
errorTitle.textContent = 'Not Found';
|
||||
errorMessage.textContent = 'The requested page was not found.';
|
||||
errorAction.textContent = 'Close Window';
|
||||
errorAction.addEventListener('click', function() { window.close(); });
|
||||
break;
|
||||
default:
|
||||
errorTitle.textContent = 'Error';
|
||||
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
|
||||
errorAction.textContent = 'Close Window';
|
||||
errorAction.addEventListener('click', function() { window.close(); });
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Extract token from URL path and set form action
|
||||
var pathParts = window.location.pathname.split('/');
|
||||
var token = pathParts[pathParts.length - 1];
|
||||
// Validate token is hex-only to prevent injection
|
||||
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;
|
||||
|
||||
// Check if OIDC is available and configure SSO button
|
||||
(async function() {
|
||||
try {
|
||||
var resp = await fetch('/api/auth/oidc/providers');
|
||||
if (!resp.ok) return;
|
||||
var info = await resp.json();
|
||||
if (!info.enabled) return;
|
||||
|
||||
// Show OIDC section
|
||||
document.getElementById('oidc-section').classList.remove('hidden');
|
||||
|
||||
// Update button text with provider name
|
||||
var btn = document.getElementById('oidc-button');
|
||||
btn.textContent = 'Sign in with ' + (info.provider_name || 'SSO');
|
||||
|
||||
// If password login is disabled, hide the password form
|
||||
if (!info.password_login_enabled) {
|
||||
document.getElementById('login-flow-form').style.display = 'none';
|
||||
}
|
||||
|
||||
// SSO button redirects to the OIDC flow for this NC token
|
||||
btn.addEventListener('click', function() {
|
||||
window.location.href = '/login/v2/flow/' + token + '/oidc';
|
||||
});
|
||||
} catch(e) {
|
||||
// OIDC not available — silently keep password-only mode
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,7 @@
|
||||
document.getElementById('close-window-btn').addEventListener('click', function() {
|
||||
window.close();
|
||||
});
|
||||
// Auto-close after 3 seconds
|
||||
setTimeout(function() {
|
||||
window.close();
|
||||
}, 3000);
|
||||
@@ -62,7 +62,7 @@ async function init() {
|
||||
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';
|
||||
document.getElementById('password-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
loadAppPasswords();
|
||||
@@ -72,7 +72,7 @@ async function init() {
|
||||
if (oidcResp.ok) {
|
||||
const oidcInfo = await oidcResp.json();
|
||||
if (!oidcInfo.password_login_enabled) {
|
||||
document.getElementById('password-section').style.display = 'none';
|
||||
document.getElementById('password-section').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
} catch (oidcErr) {
|
||||
@@ -157,7 +157,7 @@ function renderPwRow(pw) {
|
||||
btn.className = 'btn btn-danger-sm';
|
||||
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||
btn.title = 'Revoke';
|
||||
btn.onclick = function () { revokeAppPassword(pw.id, pw.label); };
|
||||
btn.addEventListener('click', function () { revokeAppPassword(pw.id, pw.label); });
|
||||
actions.appendChild(btn);
|
||||
tr.append(label, created, lastUsed, actions);
|
||||
return tr;
|
||||
@@ -167,7 +167,7 @@ async function loadAppPasswords() {
|
||||
try {
|
||||
const resp = await fetch(API + '/auth/app-passwords', { headers: headers() });
|
||||
if (!resp.ok) {
|
||||
document.getElementById('app-passwords-section').style.display = 'none';
|
||||
document.getElementById('app-passwords-section').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
const passwords = await resp.json();
|
||||
@@ -180,20 +180,20 @@ async function loadAppPasswords() {
|
||||
const empty = document.getElementById('app-pw-empty');
|
||||
tbody.innerHTML = '';
|
||||
if (userPws.length === 0) {
|
||||
table.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
table.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
} else {
|
||||
table.style.display = '';
|
||||
empty.style.display = 'none';
|
||||
table.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
for (const pw of userPws) tbody.appendChild(renderPwRow(pw));
|
||||
}
|
||||
|
||||
// Auto-generated (client session) passwords
|
||||
const autoSection = document.getElementById('app-pw-auto-section');
|
||||
if (autoPws.length === 0) {
|
||||
autoSection.style.display = 'none';
|
||||
autoSection.classList.add('hidden');
|
||||
} else {
|
||||
autoSection.style.display = '';
|
||||
autoSection.classList.remove('hidden');
|
||||
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
|
||||
const autoTbody = document.getElementById('app-pw-auto-tbody');
|
||||
autoTbody.innerHTML = '';
|
||||
@@ -207,9 +207,9 @@ async function loadAppPasswords() {
|
||||
function toggleAutoPasswords() {
|
||||
const body = document.getElementById('app-pw-auto-body');
|
||||
const chevron = document.getElementById('app-pw-auto-chevron');
|
||||
const open = body.style.display === 'none';
|
||||
body.style.display = open ? '' : 'none';
|
||||
chevron.className = open ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
|
||||
const isHidden = body.classList.contains('hidden');
|
||||
body.classList.toggle('hidden', !isHidden);
|
||||
chevron.className = isHidden ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
|
||||
}
|
||||
|
||||
async function createAppPassword() {
|
||||
@@ -241,7 +241,7 @@ async function createAppPassword() {
|
||||
const result = await resp.json();
|
||||
document.getElementById('app-pw-created-label').textContent = result.label;
|
||||
document.getElementById('app-pw-created-password').textContent = result.password;
|
||||
document.getElementById('app-pw-created').style.display = 'block';
|
||||
document.getElementById('app-pw-created').classList.remove('hidden');
|
||||
labelInput.value = '';
|
||||
loadAppPasswords();
|
||||
} catch (err) {
|
||||
@@ -255,7 +255,7 @@ async function createAppPassword() {
|
||||
function copyAppPassword() {
|
||||
const pw = document.getElementById('app-pw-created-password').textContent;
|
||||
navigator.clipboard.writeText(pw).then(function () {
|
||||
const btn = document.querySelector('.btn-copy');
|
||||
const btn = document.getElementById('app-pw-copy-btn');
|
||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
setTimeout(function () { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 1500);
|
||||
});
|
||||
@@ -269,7 +269,7 @@ async function revokeAppPassword(id, label) {
|
||||
headers: headers()
|
||||
});
|
||||
if (resp.ok || resp.status === 204) {
|
||||
document.getElementById('app-pw-created').style.display = 'none';
|
||||
document.getElementById('app-pw-created').classList.add('hidden');
|
||||
loadAppPasswords();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
@@ -280,7 +280,16 @@ async function revokeAppPassword(id, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
/* Wire up form handler (replaces inline onsubmit) */
|
||||
/* Wire up event handlers (replaces inline onclick/onsubmit) */
|
||||
document.getElementById('password-form').addEventListener('submit', changePassword);
|
||||
document.getElementById('app-pw-generate').addEventListener('click', createAppPassword);
|
||||
document.getElementById('app-pw-copy-btn').addEventListener('click', copyAppPassword);
|
||||
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
|
||||
|
||||
@@ -92,14 +92,14 @@ const sharedView = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="empty-shared-state" class="empty-state" style="display:none;">
|
||||
<i class="fas fa-share-alt" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
||||
<div id="empty-shared-state" class="empty-state hidden">
|
||||
<i class="fas fa-share-alt empty-state-icon"></i>
|
||||
<p data-i18n="shared_emptyStateTitle">No shared items</p>
|
||||
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
|
||||
<button id="go-to-files-btn" class="button primary" data-i18n="shared.goToFiles">Go to Files</button>
|
||||
</div>
|
||||
|
||||
<div class="shared-list-container" style="display:none;">
|
||||
<div class="shared-list-container hidden">
|
||||
<table class="shared-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
Reference in New Issue
Block a user