/* Administrator account management. */
let managedUsers = [];
// 显示时翻译;查不到就回落原始值(后端加了新角色也不会显示成空白)
const ROLE_LABEL_KEYS = { admin: 'role_admin', engineer: 'role_engineer', reviewer: 'role_reviewer' };
const STATUS_LABEL_KEYS = { active: 'status_active', disabled: 'status_disabled', expired: 'status_expired' };
const ROLE_CLASSES = {
admin: 'text-blue-400 bg-blue-900/30 border-blue-800',
engineer: 'text-purple-400 bg-purple-900/30 border-purple-800',
reviewer: 'text-green-400 bg-green-900/30 border-green-800',
};
function escUser(value) {
return String(value ?? '').replace(/[&<>"']/g, c => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[c]));
}
function showUserAlert(message, error = false) {
const el = document.getElementById('user-alert');
if (!el) return;
el.textContent = message;
el.className = `mb-4 rounded-lg border px-4 py-3 text-sm ${error
? 'text-red-300 bg-red-900/20 border-red-800'
: 'text-green-300 bg-green-900/20 border-green-800'}`;
window.clearTimeout(showUserAlert.timer);
showUserAlert.timer = window.setTimeout(() => el.classList.add('hidden'), 4000);
}
async function apiUsers(url, options = {}) {
const response = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...options });
let data = {};
try { data = await response.json(); } catch (_) {}
if (!response.ok) throw new Error(data.detail || `Request failed (${response.status})`);
return data;
}
async function loadUsers() {
const role = document.getElementById('user-role-filter')?.value || '';
const status = document.getElementById('user-status-filter')?.value || '';
const params = new URLSearchParams();
if (role) params.set('role', role);
if (status) params.set('status', status);
try {
const data = await apiUsers(`/api/v1/users?${params}`);
managedUsers = data.users || [];
updateUserCounts();
renderUsers();
} catch (error) {
showUserAlert(error.message, true);
document.getElementById('users-body').innerHTML = `
| ${escUser(error.message)} |
`;
}
}
function updateUserCounts() {
const active = managedUsers.filter(user => user.status === 'active').length;
document.getElementById('count-total').textContent = managedUsers.length;
document.getElementById('count-active').textContent = active;
document.getElementById('count-inactive').textContent = managedUsers.length - active;
}
function formatDate(value) {
if (!value) return 'Never';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();
}
function renderUsers() {
const query = (document.getElementById('user-search')?.value || '').trim().toLowerCase();
const users = managedUsers.filter(user => !query
|| user.username.toLowerCase().includes(query)
|| (user.email || '').toLowerCase().includes(query));
document.getElementById('user-list-count').textContent = t('users_count').replace('%s', users.length);
const body = document.getElementById('users-body');
if (!users.length) {
body.innerHTML = '| ' + t('users_no_found') + ' |
';
return;
}
body.innerHTML = users.map(user => {
const statusClass = user.status === 'active'
? 'text-green-400 bg-green-900/30 border-green-800'
: user.status === 'expired'
? 'text-yellow-400 bg-yellow-900/30 border-yellow-800'
: 'text-gray-400 bg-gray-800 border-gray-700';
let action = '';
if (user.status === 'expired') {
action = ``;
} else if (user.account_status === 'active') {
action = ``;
} else {
action = ``;
}
return `
${escUser(user.username)} ${escUser(user.email || t('users_no_email'))} |
${escUser(ROLE_LABEL_KEYS[user.role] ? t(ROLE_LABEL_KEYS[user.role]) : user.role)} |
${escUser(STATUS_LABEL_KEYS[user.status] ? t(STATUS_LABEL_KEYS[user.status]) : user.status)} |
${escUser(formatDate(user.last_login_at))} |
${escUser(user.expires_at ? formatDate(user.expires_at) : t('users_never'))} |
${action} |
`;
}).join('');
}
function toLocalInput(value) {
if (!value) return '';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return '';
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function openUserModal(userId = '') {
const user = managedUsers.find(item => item.id === userId);
document.getElementById('user-id').value = user?.id || '';
document.getElementById('user-modal-title').textContent = user ? t('users_edit_user') : t('users_new');
document.getElementById('user-save-btn').textContent = user ? t('users_save_changes') : t('users_create');
document.getElementById('user-username').value = user?.username || '';
document.getElementById('user-username').disabled = Boolean(user);
document.getElementById('user-role').value = user?.role || 'engineer';
document.getElementById('user-email').value = user?.email || '';
document.getElementById('user-password').value = '';
document.getElementById('user-password').required = !user;
document.getElementById('password-hint').textContent = user ? t('users_pw_keep') : t('users_pw_hint');
document.getElementById('user-status').value = user?.account_status || 'active';
document.getElementById('user-expires').value = toLocalInput(user?.expires_at);
document.getElementById('user-modal').classList.remove('hidden');
setTimeout(() => document.getElementById(user ? 'user-email' : 'user-username').focus(), 0);
}
function closeUserModal() { document.getElementById('user-modal').classList.add('hidden'); }
async function saveUser(event) {
event.preventDefault();
const id = document.getElementById('user-id').value;
const expires = document.getElementById('user-expires').value;
const payload = {
email: document.getElementById('user-email').value.trim() || null,
role: document.getElementById('user-role').value,
expires_at: expires ? new Date(expires).toISOString() : null,
};
const password = document.getElementById('user-password').value;
if (password) payload.password = password;
if (id) {
payload.status = document.getElementById('user-status').value;
} else {
payload.username = document.getElementById('user-username').value.trim();
payload.password = password;
}
const button = document.getElementById('user-save-btn');
button.disabled = true;
try {
await apiUsers(id ? `/api/v1/users/${encodeURIComponent(id)}` : '/api/v1/users', {
method: id ? 'PATCH' : 'POST', body: JSON.stringify(payload)
});
closeUserModal();
showUserAlert(id ? 'User updated' : 'User created');
await loadUsers();
} catch (error) {
showUserAlert(error.message, true);
} finally { button.disabled = false; }
}
async function toggleUser(id, status) {
try {
await apiUsers(`/api/v1/users/${encodeURIComponent(id)}`, { method: 'PATCH', body: JSON.stringify({ status }) });
showUserAlert(status === 'active' ? 'User enabled' : 'User disabled');
await loadUsers();
} catch (error) { showUserAlert(error.message, true); }
}
async function deleteUser(id) {
const user = managedUsers.find(item => item.id === id);
if (!user || !window.confirm(t('users_confirm_delete').replace('%s', user.username))) return;
try {
await apiUsers(`/api/v1/users/${encodeURIComponent(id)}`, { method: 'DELETE' });
showUserAlert('User deleted');
await loadUsers();
} catch (error) { showUserAlert(error.message, true); }
}
document.addEventListener('DOMContentLoaded', loadUsers);