style: apply biome CSS/JS format
This commit is contained in:
+649
-391
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user