/**
* Requestarr User Management Module
* Handles user list, create/edit/delete, Plex import, and permissions.
*/
window.RequestarrUsers = {
users: [],
permissionLabels: {
request_movies: 'Request Movies',
request_tv: 'Request TV',
auto_approve: 'Auto Approve All',
auto_approve_movies: 'Auto Approve Movies',
auto_approve_tv: 'Auto Approve TV',
manage_requests: 'Manage Requests',
manage_users: 'Manage Users',
view_requests: 'View All Requests',
hide_media_global: 'Hide Media (Global)',
disable_chat: 'Disable Chat',
},
async init() {
await this.loadUsers();
},
async loadUsers() {
const container = document.getElementById('requestarr-users-view');
if (!container) return;
try {
const resp = await fetch('./api/requestarr/users', { cache: 'no-store' });
if (!resp.ok) throw new Error('Failed to load users');
const data = await resp.json();
this.users = data.users || [];
this.render();
} catch (e) {
console.error('[RequestarrUsers] Error loading users:', e);
this.renderError();
}
},
render() {
const container = document.getElementById('requsers-content');
if (!container) return;
const rows = this.users.map(u => {
const initials = (u.username || '?').substring(0, 2).toUpperCase();
const avatarHtml = u.avatar_url
? ``
: initials;
const roleClass = `requsers-role-${u.role || 'user'}`;
const joined = u.created_at ? new Date(u.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '—';
const isOwner = u.role === 'owner';
return `
| User | Requests | Role | Joined | Actions |
|---|---|---|---|---|
| No users found | ||||
Failed to load users. Check your connection.
'; } }, // ── Create User Modal ──────────────────────────────────── openCreateModal() { this._openModal('Create Local User', null); }, openEditModal(userId) { const user = this.users.find(u => u.id === userId); if (!user) return; this._openModal('Edit User', user); }, _openModal(title, user) { const isEdit = !!user; const isOwner = isEdit && user.role === 'owner'; const perms = (user && typeof user.permissions === 'object') ? user.permissions : {}; const permsHtml = Object.entries(this.permissionLabels).map(([key, label]) => { const checked = perms[key] ? 'checked' : ''; const disabled = isOwner ? 'disabled' : ''; // Hide disable_chat for owner — owner can never be chat-disabled if (key === 'disable_chat' && isOwner) return ''; return ``; }).join(''); // Hide password field for owner const passwordFieldHtml = isOwner ? '' : ` `; const html = ``; // Remove existing modal if any this.closeModal(); document.body.insertAdjacentHTML('beforeend', html); }, closeModal() { const overlay = document.getElementById('requsers-modal-overlay'); if (overlay) overlay.remove(); const plexOverlay = document.getElementById('requsers-plex-modal-overlay'); if (plexOverlay) plexOverlay.remove(); }, async fillGeneratedPassword() { try { const resp = await fetch('./api/requestarr/users/generate-password'); const data = await resp.json(); const input = document.getElementById('requsers-modal-password'); if (input && data.password) { input.type = 'text'; input.value = data.password; // Copy to clipboard try { await navigator.clipboard.writeText(data.password); } catch (_) {} if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Password generated and copied to clipboard', 'success'); } } catch (e) { console.error('[RequestarrUsers] Error generating password:', e); } }, async onRoleChange() { // Load default permissions for the selected role try { const resp = await fetch('./api/requestarr/users/permissions-template'); const templates = await resp.json(); const role = document.getElementById('requsers-modal-role').value; const perms = templates[role] || {}; const grid = document.getElementById('requsers-perms-grid'); if (!grid) return; grid.querySelectorAll('input[type="checkbox"]').forEach(cb => { const key = cb.name.replace('perm_', ''); cb.checked = !!perms[key]; }); } catch (_) {} }, async saveUser(userId) { const username = (document.getElementById('requsers-modal-username').value || '').trim(); const email = (document.getElementById('requsers-modal-email').value || '').trim(); const passwordEl = document.getElementById('requsers-modal-password'); const password = passwordEl ? passwordEl.value : ''; const role = document.getElementById('requsers-modal-role').value; // Collect permissions const permissions = {}; const grid = document.getElementById('requsers-perms-grid'); if (grid) { grid.querySelectorAll('input[type="checkbox"]').forEach(cb => { const key = cb.name.replace('perm_', ''); permissions[key] = cb.checked; }); } if (!username || username.length < 3) { if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Username must be at least 3 characters', 'error'); return; } const body = { username, email, role, permissions }; if (password) body.password = password; const isEdit = userId !== null; if (!isEdit && (!password || password.length < 8)) { if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Password must be at least 8 characters', 'error'); return; } const saveBtn = document.getElementById('requsers-modal-save'); if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; } try { const url = isEdit ? `./api/requestarr/users/${userId}` : './api/requestarr/users'; const method = isEdit ? 'PUT' : 'POST'; const resp = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await resp.json(); if (data.success) { this.closeModal(); if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(isEdit ? 'User updated' : 'User created', 'success'); await this.loadUsers(); } else { if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(data.error || 'Failed to save user', 'error'); } } catch (e) { console.error('[RequestarrUsers] Error saving user:', e); if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('Failed to save user', 'error'); } finally { if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = isEdit ? 'Save Changes' : 'Create User'; } } }, confirmDelete(userId, username) { if (window.HuntarrConfirmModal && typeof window.HuntarrConfirmModal.show === 'function') { window.HuntarrConfirmModal.show({ title: 'Delete User', message: `Are you sure you want to delete ${this._esc(username)}? This cannot be undone.`, confirmText: 'Delete', confirmClass: 'danger', onConfirm: () => this.deleteUser(userId), }); } else { if (confirm(`Delete user "${username}"? This cannot be undone.`)) { this.deleteUser(userId); } } }, async deleteUser(userId) { try { const resp = await fetch(`./api/requestarr/users/${userId}`, { method: 'DELETE' }); const data = await resp.json(); if (data.success) { if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('User deleted', 'success'); await this.loadUsers(); } else { if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification(data.error || 'Failed to delete user', 'error'); } } catch (e) { console.error('[RequestarrUsers] Error deleting user:', e); } }, // ── Plex Import ────────────────────────────────────────── async openPlexImportModal() { try { const resp = await fetch('./api/requestarr/users/plex/friends'); const data = await resp.json(); if (data.error) { // No Plex linked — offer to link it right here via popup if (window.HuntarrConfirm && window.HuntarrConfirm.show) { window.HuntarrConfirm.show({ title: 'Plex Account Not Linked', message: 'No Plex account is linked. Would you like to link your Plex account now? Once linked, you can import your Plex users.', confirmLabel: 'Link Plex', onConfirm: () => { this._startPlexLinkFromUsers(); } }); } return; } const allUsers = data.friends || []; // Split into importable and already-imported const importable = allUsers.filter(f => !f.already_imported); const alreadyImported = allUsers.filter(f => f.already_imported); if (!allUsers.length) { if (window.HuntarrNotifications) window.HuntarrNotifications.showNotification('No Plex users found with server access', 'info'); return; } const renderUserRow = (f, disabled) => { const initial = (f.username || '?').charAt(0).toUpperCase(); const avatarHtml = f.thumb ? `