style: apply biome CSS/JS format

This commit is contained in:
Edouard Vanbelle
2026-04-07 22:48:59 +02:00
parent 786a778546
commit 1ce29c101d
86 changed files with 18032 additions and 15166 deletions
File diff suppressed because it is too large Load Diff
+117 -109
View File
@@ -1,109 +1,117 @@
// device-verify.js — Extracted from inline <script> in device-verify.html
(function () {
'use strict';
var API_BASE = window.location.origin;
var codeInput = document.getElementById('user-code');
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 debounceTimer = null;
var currentCode = '';
// Pre-fill from URL query param (?code=ABCD-1234)
var params = new URLSearchParams(window.location.search);
if (params.get('code')) {
codeInput.value = params.get('code');
lookupCode(params.get('code'));
}
// Auto-insert hyphen and lookup on input
codeInput.addEventListener('input', function (e) {
var val = e.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;
errorText.classList.add('hidden');
// Debounce lookup
clearTimeout(debounceTimer);
if (val.length >= 9) {
debounceTimer = setTimeout(function () { lookupCode(val); }, 300);
} else {
deviceInfo.classList.add('hidden');
actionButtons.classList.add('hidden');
}
});
// Wire up approve / deny buttons (replaces inline onclick)
btnApprove.addEventListener('click', function () { handleAction('approve'); });
btnDeny.addEventListener('click', function () { handleAction('deny'); });
async function lookupCode(code) {
try {
var resp = await fetch(API_BASE + '/api/auth/device/verify?code=' + encodeURIComponent(code), {
credentials: 'same-origin'
});
if (resp.status === 401) {
showError('You must be logged in to authorize a device. Please log in first.');
return;
}
if (!resp.ok) throw new Error('Lookup failed');
var data = await resp.json();
if (data.valid) {
currentCode = code;
document.getElementById('info-client').textContent = data.client_name || 'Unknown';
document.getElementById('info-scopes').textContent = data.scopes || 'all';
deviceInfo.classList.remove('hidden');
actionButtons.classList.remove('hidden');
errorText.classList.add('hidden');
} else {
deviceInfo.classList.add('hidden');
actionButtons.classList.add('hidden');
showError('Code not found or expired. Please check and try again.');
}
} catch (_err) {
showError('Failed to verify code. Please try again.');
}
}
async function handleAction(action) {
btnApprove.disabled = true;
btnDeny.disabled = true;
try {
var resp = await fetch(API_BASE + '/api/auth/device/verify', {
method: 'POST',
credentials: 'same-origin',
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
body: JSON.stringify({ user_code: currentCode, action: action })
});
if (!resp.ok) {
var err = await resp.json().catch(function () { return {}; });
throw new Error(err.message || 'Action failed');
}
document.getElementById('step-code').classList.add('hidden');
if (action === 'approve') {
document.getElementById('status-success').classList.remove('hidden');
} else {
document.getElementById('status-denied').classList.remove('hidden');
}
} catch (err) {
btnApprove.disabled = false;
btnDeny.disabled = false;
showError(err.message || 'Failed to process action.');
}
}
function showError(msg) {
errorText.textContent = msg;
errorText.classList.remove('hidden');
}
})();
// device-verify.js — Extracted from inline <script> in device-verify.html
(function () {
'use strict';
var API_BASE = window.location.origin;
var codeInput = document.getElementById('user-code');
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 debounceTimer = null;
var currentCode = '';
// Pre-fill from URL query param (?code=ABCD-1234)
var params = new URLSearchParams(window.location.search);
if (params.get('code')) {
codeInput.value = params.get('code');
lookupCode(params.get('code'));
}
// Auto-insert hyphen and lookup on input
codeInput.addEventListener('input', function (e) {
var val = e.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;
errorText.classList.add('hidden');
// Debounce lookup
clearTimeout(debounceTimer);
if (val.length >= 9) {
debounceTimer = setTimeout(function () {
lookupCode(val);
}, 300);
} else {
deviceInfo.classList.add('hidden');
actionButtons.classList.add('hidden');
}
});
// Wire up approve / deny buttons (replaces inline onclick)
btnApprove.addEventListener('click', function () {
handleAction('approve');
});
btnDeny.addEventListener('click', function () {
handleAction('deny');
});
async function lookupCode(code) {
try {
var resp = await fetch(API_BASE + '/api/auth/device/verify?code=' + encodeURIComponent(code), {
credentials: 'same-origin'
});
if (resp.status === 401) {
showError('You must be logged in to authorize a device. Please log in first.');
return;
}
if (!resp.ok) throw new Error('Lookup failed');
var data = await resp.json();
if (data.valid) {
currentCode = code;
document.getElementById('info-client').textContent = data.client_name || 'Unknown';
document.getElementById('info-scopes').textContent = data.scopes || 'all';
deviceInfo.classList.remove('hidden');
actionButtons.classList.remove('hidden');
errorText.classList.add('hidden');
} else {
deviceInfo.classList.add('hidden');
actionButtons.classList.add('hidden');
showError('Code not found or expired. Please check and try again.');
}
} catch (_err) {
showError('Failed to verify code. Please try again.');
}
}
async function handleAction(action) {
btnApprove.disabled = true;
btnDeny.disabled = true;
try {
var resp = await fetch(API_BASE + '/api/auth/device/verify', {
method: 'POST',
credentials: 'same-origin',
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
body: JSON.stringify({ user_code: currentCode, action: action })
});
if (!resp.ok) {
var err = await resp.json().catch(function () {
return {};
});
throw new Error(err.message || 'Action failed');
}
document.getElementById('step-code').classList.add('hidden');
if (action === 'approve') {
document.getElementById('status-success').classList.remove('hidden');
} else {
document.getElementById('status-denied').classList.remove('hidden');
}
} catch (err) {
btnApprove.disabled = false;
btnDeny.disabled = false;
showError(err.message || 'Failed to process action.');
}
}
function showError(msg) {
errorText.textContent = msg;
errorText.classList.remove('hidden');
}
})();
+13 -5
View File
@@ -6,28 +6,36 @@ var errorTitle = document.getElementById('error-title');
var errorMessage = document.getElementById('error-message');
var errorAction = document.getElementById('error-action');
switch(errorType) {
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(); });
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(); });
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(); });
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(); });
errorAction.addEventListener('click', function () {
window.close();
});
}
+3 -3
View File
@@ -9,7 +9,7 @@ if (!/^[0-9a-fA-F]+$/.test(token)) {
document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
// Check if OIDC is available and configure SSO button
(async function() {
(async function () {
try {
var resp = await fetch('/api/auth/oidc/providers');
if (!resp.ok) return;
@@ -29,10 +29,10 @@ document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
}
// SSO button redirects to the OIDC flow for this NC token
btn.addEventListener('click', function() {
btn.addEventListener('click', function () {
window.location.href = '/login/v2/flow/' + token + '/oidc';
});
} catch(e) {
} catch (e) {
// OIDC not available — silently keep password-only mode
}
})();
+2 -2
View File
@@ -1,7 +1,7 @@
document.getElementById('close-window-btn').addEventListener('click', function() {
document.getElementById('close-window-btn').addEventListener('click', function () {
window.close();
});
// Auto-close after 3 seconds
setTimeout(function() {
setTimeout(function () {
window.close();
}, 3000);
+283 -247
View File
@@ -2,144 +2,163 @@ const API = '/api';
/* ── i18n helper — falls back to key if i18n not ready ── */
function t(key, params) {
if (window.i18n && typeof window.i18n.t === 'function') return window.i18n.t(key, params);
return key.split('.').pop().replace(/_/g, ' ');
if (window.i18n && typeof window.i18n.t === 'function') return window.i18n.t(key, params);
return key.split('.').pop().replace(/_/g, ' ');
}
function headers() {
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
}
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];
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 t('profile.never');
const d = new Date(dateStr);
const now = new Date();
const secs = Math.floor((now - d) / 1000);
if (secs < 60) return t('profile.just_now');
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs/60) });
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs/3600) });
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs/86400) });
return d.toLocaleDateString();
if (!dateStr) return t('profile.never');
const d = new Date(dateStr);
const now = new Date();
const secs = Math.floor((now - d) / 1000);
if (secs < 60) return t('profile.just_now');
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs / 60) });
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs / 3600) });
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs / 86400) });
return d.toLocaleDateString();
}
async function init() {
try {
const resp = await fetch(API + '/auth/me', { headers: headers(), credentials: 'same-origin' });
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> ' + t('profile.role_admin');
} else {
badge.className = 'role-badge role-badge-user';
badge.innerHTML = '<i class="fas fa-user"></i> ' + t('profile.role_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' ? t('profile.role_admin') : t('profile.role_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) : t('profile.unlimited'));
if (user.auth_provider && user.auth_provider !== 'local') {
document.getElementById('password-section').classList.add('hidden');
}
loadAppPasswords();
try {
const oidcResp = await fetch(API + '/auth/oidc/providers', { credentials: 'same-origin' });
if (oidcResp.ok) {
const oidcInfo = await oidcResp.json();
if (!oidcInfo.password_login_enabled) {
document.getElementById('password-section').classList.add('hidden');
const resp = await fetch(API + '/auth/me', {
headers: headers(),
credentials: 'same-origin'
});
if (!resp.ok) {
showError();
return;
}
}
} catch (oidcErr) {
}
const user = await resp.json();
document.getElementById('loading').classList.add('hidden');
document.getElementById('main-content').classList.remove('hidden');
} catch (e) {
console.error(e);
showError();
}
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> ' + t('profile.role_admin');
} else {
badge.className = 'role-badge role-badge-user';
badge.innerHTML = '<i class="fas fa-user"></i> ' + t('profile.role_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' ? t('profile.role_admin') : t('profile.role_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) : t('profile.unlimited'));
if (user.auth_provider && user.auth_provider !== 'local') {
document.getElementById('password-section').classList.add('hidden');
}
loadAppPasswords();
try {
const oidcResp = await fetch(API + '/auth/oidc/providers', {
credentials: 'same-origin'
});
if (oidcResp.ok) {
const oidcInfo = await oidcResp.json();
if (!oidcInfo.password_login_enabled) {
document.getElementById('password-section').classList.add('hidden');
}
}
} catch (oidcErr) {}
document.getElementById('loading').classList.add('hidden');
document.getElementById('main-content').classList.remove('hidden');
} catch (e) {
console.error(e);
showError();
}
}
function showError() {
document.getElementById('loading').classList.add('hidden');
document.getElementById('auth-error').classList.remove('hidden');
document.getElementById('loading').classList.add('hidden');
document.getElementById('auth-error').classList.remove('hidden');
}
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');
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> ' + escapeHtml(t('profile.passwords_no_match')) + '</div>';
return false;
}
if (newPw.length < 8) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.password_too_short')) + '</div>';
return false;
}
const btn = document.getElementById('pw-submit');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.updating'));
try {
const resp = await fetch(API + '/auth/change-password', {
method: 'PUT',
headers: headers(),
credentials: 'same-origin',
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> ' + escapeHtml(t('profile.password_updated')) + '</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> ' + escapeHtml(err.message || t('profile.password_change_failed')) + '</div>';
if (newPw !== confirmPw) {
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.passwords_no_match')) + '</div>';
return false;
}
} catch (err) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_network', { message: err.message })) + '</div>';
}
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('profile.update_password'));
return false;
if (newPw.length < 8) {
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.password_too_short')) + '</div>';
return false;
}
const btn = document.getElementById('pw-submit');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.updating'));
try {
const resp = await fetch(API + '/auth/change-password', {
method: 'PUT',
headers: headers(),
credentials: 'same-origin',
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> ' + escapeHtml(t('profile.password_updated')) + '</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> ' +
escapeHtml(err.message || t('profile.password_change_failed')) +
'</div>';
}
} catch (err) {
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(t('profile.error_network', { message: err.message })) +
'</div>';
}
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('profile.update_password'));
return false;
}
// ── App Passwords ──
@@ -147,164 +166,177 @@ async function changePassword(e) {
const AUTO_LABELS = ['Nextcloud', 'Nextcloud (OIDC)'];
function isAutoPassword(pw) {
return AUTO_LABELS.includes(pw.label);
return AUTO_LABELS.includes(pw.label);
}
function renderPwRow(pw) {
const tr = document.createElement('tr');
const label = document.createElement('td');
label.textContent = pw.label;
const created = document.createElement('td');
created.textContent = new Date(pw.created_at).toLocaleDateString();
const lastUsed = document.createElement('td');
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : t('profile.never');
const status = document.createElement('td');
const badge = document.createElement('span');
if (pw.active !== false) {
badge.className = 'badge badge-active';
badge.textContent = t('profile.active');
} else {
badge.className = 'badge badge-expired';
badge.textContent = t('profile.revoked');
}
status.appendChild(badge);
const actions = document.createElement('td');
if (pw.active !== false) {
const btn = document.createElement('button');
btn.className = 'btn btn-danger-sm';
btn.innerHTML = '<i class="fas fa-trash"></i>';
btn.title = t('profile.revoke_title');
btn.addEventListener('click', function () { revokeAppPassword(pw.id, pw.label); });
actions.appendChild(btn);
}
tr.append(label, created, lastUsed, status, actions);
return tr;
const tr = document.createElement('tr');
const label = document.createElement('td');
label.textContent = pw.label;
const created = document.createElement('td');
created.textContent = new Date(pw.created_at).toLocaleDateString();
const lastUsed = document.createElement('td');
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : t('profile.never');
const status = document.createElement('td');
const badge = document.createElement('span');
if (pw.active !== false) {
badge.className = 'badge badge-active';
badge.textContent = t('profile.active');
} else {
badge.className = 'badge badge-expired';
badge.textContent = t('profile.revoked');
}
status.appendChild(badge);
const actions = document.createElement('td');
if (pw.active !== false) {
const btn = document.createElement('button');
btn.className = 'btn btn-danger-sm';
btn.innerHTML = '<i class="fas fa-trash"></i>';
btn.title = t('profile.revoke_title');
btn.addEventListener('click', function () {
revokeAppPassword(pw.id, pw.label);
});
actions.appendChild(btn);
}
tr.append(label, created, lastUsed, status, actions);
return tr;
}
async function loadAppPasswords() {
try {
const resp = await fetch(API + '/auth/app-passwords', { headers: headers(), credentials: 'same-origin' });
if (!resp.ok) {
document.getElementById('app-passwords-section').classList.add('hidden');
return;
}
const data = await resp.json();
const passwords = data.app_passwords || data;
const userPws = passwords.filter(function (pw) { return !isAutoPassword(pw); });
const autoPws = passwords.filter(isAutoPassword);
try {
const resp = await fetch(API + '/auth/app-passwords', {
headers: headers(),
credentials: 'same-origin'
});
if (!resp.ok) {
document.getElementById('app-passwords-section').classList.add('hidden');
return;
}
const data = await resp.json();
const passwords = data.app_passwords || data;
const userPws = passwords.filter(function (pw) {
return !isAutoPassword(pw);
});
const autoPws = passwords.filter(isAutoPassword);
// User-created passwords
const tbody = document.getElementById('app-pw-tbody');
const table = document.getElementById('app-pw-table');
const empty = document.getElementById('app-pw-empty');
tbody.innerHTML = '';
if (userPws.length === 0) {
table.classList.add('hidden');
empty.classList.remove('hidden');
} else {
table.classList.remove('hidden');
empty.classList.add('hidden');
for (const pw of userPws) tbody.appendChild(renderPwRow(pw));
}
// User-created passwords
const tbody = document.getElementById('app-pw-tbody');
const table = document.getElementById('app-pw-table');
const empty = document.getElementById('app-pw-empty');
tbody.innerHTML = '';
if (userPws.length === 0) {
table.classList.add('hidden');
empty.classList.remove('hidden');
} else {
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.classList.add('hidden');
} else {
autoSection.classList.remove('hidden');
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
const autoTbody = document.getElementById('app-pw-auto-tbody');
autoTbody.innerHTML = '';
for (const pw of autoPws) autoTbody.appendChild(renderPwRow(pw));
// Auto-generated (client session) passwords
const autoSection = document.getElementById('app-pw-auto-section');
if (autoPws.length === 0) {
autoSection.classList.add('hidden');
} else {
autoSection.classList.remove('hidden');
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
const autoTbody = document.getElementById('app-pw-auto-tbody');
autoTbody.innerHTML = '';
for (const pw of autoPws) autoTbody.appendChild(renderPwRow(pw));
}
} catch (e) {
console.error('Failed to load app passwords', e);
}
} catch (e) {
console.error('Failed to load app passwords', e);
}
}
function toggleAutoPasswords() {
const body = document.getElementById('app-pw-auto-body');
const chevron = document.getElementById('app-pw-auto-chevron');
const isHidden = body.classList.contains('hidden');
body.classList.toggle('hidden', !isHidden);
chevron.className = isHidden ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
const body = document.getElementById('app-pw-auto-body');
const chevron = document.getElementById('app-pw-auto-chevron');
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() {
const labelInput = 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 labelInput = document.getElementById('app-pw-label');
const label = labelInput.value.trim();
const statusEl = document.getElementById('app-pw-status');
const btn = document.getElementById('app-pw-generate');
if (!label) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_label_required')) + '</div>';
return;
}
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.generating'));
statusEl.innerHTML = '';
try {
const resp = await fetch(API + '/auth/app-passwords', {
method: 'POST',
headers: headers(),
credentials: 'same-origin',
body: JSON.stringify({ label: label })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(err.message || t('profile.error_create_pw')) + '</div>';
return;
if (!label) {
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_label_required')) + '</div>';
return;
}
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.generating'));
statusEl.innerHTML = '';
try {
const resp = await fetch(API + '/auth/app-passwords', {
method: 'POST',
headers: headers(),
credentials: 'same-origin',
body: JSON.stringify({ label: label })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
statusEl.innerHTML =
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
escapeHtml(err.message || t('profile.error_create_pw')) +
'</div>';
return;
}
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').classList.remove('hidden');
labelInput.value = '';
loadAppPasswords();
} catch (err) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + err.message + '</div>';
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plus"></i> ' + escapeHtml(t('profile.generate'));
}
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').classList.remove('hidden');
labelInput.value = '';
loadAppPasswords();
} catch (err) {
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + err.message + '</div>';
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plus"></i> ' + escapeHtml(t('profile.generate'));
}
}
function copyAppPassword() {
const pw = document.getElementById('app-pw-created-password').textContent;
navigator.clipboard.writeText(pw).then(function () {
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);
});
const pw = document.getElementById('app-pw-created-password').textContent;
navigator.clipboard.writeText(pw).then(function () {
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);
});
}
async function revokeAppPassword(id, label) {
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
try {
const resp = await fetch(API + '/auth/app-passwords/' + encodeURIComponent(id), {
method: 'DELETE',
headers: headers(),
credentials: 'same-origin'
});
if (resp.ok || resp.status === 204) {
document.getElementById('app-pw-created').classList.add('hidden');
loadAppPasswords();
} else {
const err = await resp.json().catch(() => ({}));
alert(err.message || t('profile.error_revoke'));
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
try {
const resp = await fetch(API + '/auth/app-passwords/' + encodeURIComponent(id), {
method: 'DELETE',
headers: headers(),
credentials: 'same-origin'
});
if (resp.ok || resp.status === 204) {
document.getElementById('app-pw-created').classList.add('hidden');
loadAppPasswords();
} else {
const err = await resp.json().catch(() => ({}));
alert(err.message || t('profile.error_revoke'));
}
} catch (err) {
alert(t('profile.error_network', { message: err.message }));
}
} catch (err) {
alert(t('profile.error_network', { message: err.message }));
}
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
var div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
}
init();
@@ -316,5 +348,9 @@ document.getElementById('app-pw-copy-btn').addEventListener('click', copyAppPass
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
/* Re-render when language changes */
window.addEventListener('translationsLoaded', function () { init(); });
window.addEventListener('localeChanged', function () { init(); });
window.addEventListener('translationsLoaded', function () {
init();
});
window.addEventListener('localeChanged', function () {
init();
});
+46 -29
View File
@@ -26,12 +26,12 @@ const sharedView = {
this.attachEventListeners();
this.loadItems().then(() => this.filterAndSortItems());
const c = document.getElementById('shared-container');
if (c) c.classList.remove("hidden");
if (c) c.classList.remove('hidden');
},
hide() {
const c = document.getElementById('shared-container');
if (c) c.classList.add("hidden");
if (c) c.classList.add('hidden');
},
// Load shared items from backend API
@@ -195,7 +195,7 @@ const sharedView = {
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
document.querySelectorAll('.shared-custom-select.open').forEach((sel) => {
if (!sel.contains(e.target)) sel.classList.remove('open');
});
});
@@ -209,16 +209,24 @@ const sharedView = {
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(); }
});
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(); }
});
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');
@@ -253,17 +261,17 @@ const sharedView = {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
// Close other open selects
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
document.querySelectorAll('.shared-custom-select.open').forEach((sel) => {
if (sel !== wrapper) sel.classList.remove('open');
});
wrapper.classList.toggle('open');
});
dropdown.querySelectorAll('.shared-select-option').forEach(option => {
dropdown.querySelectorAll('.shared-select-option').forEach((option) => {
option.addEventListener('click', (e) => {
e.stopPropagation();
// Update active state
dropdown.querySelectorAll('.shared-select-option').forEach(o => o.classList.remove('active'));
dropdown.querySelectorAll('.shared-select-option').forEach((o) => o.classList.remove('active'));
option.classList.add('active');
// Update label
const label = toggle.querySelector('.shared-select-label');
@@ -288,7 +296,7 @@ const sharedView = {
const searchInput = document.getElementById('search-input');
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
this.filteredItems = this.items.filter(item => {
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);
@@ -321,16 +329,15 @@ const sharedView = {
sharedItemsList.innerHTML = '';
if (this.filteredItems.length === 0) {
emptyState.classList.remove("hidden");
listContainer.classList.add("hidden");
emptyState.classList.remove('hidden');
listContainer.classList.add('hidden');
return;
}
emptyState.classList.add("hidden");
listContainer.classList.remove("hidden");
emptyState.classList.add('hidden');
listContainer.classList.remove('hidden');
this.filteredItems.forEach(item => {
this.filteredItems.forEach((item) => {
const row = document.createElement('tr');
const displayName = item.item_name || item.item_id || 'Unknown';
@@ -383,7 +390,8 @@ const sharedView = {
copyBtn.innerHTML = '<span class="action-icon">📋</span>';
copyBtn.title = this.translate('shared_copyLink', 'Copy Link');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(item.url)
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'));
});
@@ -392,7 +400,10 @@ const sharedView = {
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(); });
rmBtn.addEventListener('click', () => {
this.currentItem = item;
this.removeSharedItem();
});
actionsCell.append(editBtn, notifyBtn, copyBtn, rmBtn);
row.append(nameCell, typeCell, dateCell, expCell, permCell, pwCell, actionsCell);
@@ -428,7 +439,10 @@ const sharedView = {
if (enablePw) {
enablePw.checked = item.has_password;
if (pwField) { pwField.disabled = !enablePw.checked; pwField.value = ''; }
if (pwField) {
pwField.disabled = !enablePw.checked;
pwField.value = '';
}
}
if (enableExp) {
enableExp.checked = !!item.expires_at;
@@ -473,7 +487,8 @@ const sharedView = {
copyShareLink() {
const el = document.getElementById('sv-share-link-url');
if (!el) return;
navigator.clipboard.writeText(el.value)
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'));
},
@@ -514,10 +529,8 @@ const sharedView = {
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
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 {
@@ -576,8 +589,12 @@ const sharedView = {
}
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')); })
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'));
}
},