3 lines
9.0 KiB
JavaScript
3 lines
9.0 KiB
JavaScript
// User management stays separate from Smart Queue logic.
|
|
export const authUsersSource = " function renderGeneratedToken(token){\n const box=$('authTokenInline');\n if(!box) return;\n // Note: Generated tokens are shown inline to avoid stacking another modal over the Users panel.\n box.classList.remove('d-none');\n box.innerHTML=`<div class=\"d-flex justify-content-between gap-2 flex-wrap\"><div><b><i class=\"fa-solid fa-key\"></i> New API token</b><small>This token is shown once. Copy it now before refreshing the page.</small></div><button id=\"authTokenInlineClose\" class=\"btn btn-xs btn-outline-secondary\" type=\"button\"><i class=\"fa-solid fa-xmark\"></i></button></div><div class=\"input-group\"><input id=\"authTokenInlineValue\" class=\"form-control font-monospace\" value=\"${esc(token)}\" readonly><button id=\"authTokenInlineCopy\" class=\"btn btn-primary\" type=\"button\"><i class=\"fa-solid fa-copy\"></i> Copy</button></div>`;\n $('authTokenInlineCopy')?.addEventListener('click',()=>copyText(token).then(()=>toast('API token copied','success')).catch(()=>toast('Copy failed','danger')));\n $('authTokenInlineClose')?.addEventListener('click',()=>box.classList.add('d-none'));\n }\n function tokenRow(t,userId){\n const last=t.last_used_at ? humanDateCell(t.last_used_at) : '<span class=\"text-muted\">never</span>';\n return `<div class=\"api-token-row\"><div><b>${esc(t.name||'API token')}</b><small>${esc(t.token_prefix||'')} · created ${humanDateCell(t.created_at)} · last used ${last}</small></div><button class=\"btn btn-xs btn-outline-danger auth-token-delete\" data-user-id=\"${esc(userId)}\" data-token-id=\"${esc(t.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button></div>`;\n }\n async function showAuthTokens(userId){\n try{\n const j=await (await fetch(`/api/auth/users/${userId}/tokens`)).json();\n if(!j.ok) throw new Error(j.error||'Cannot load API tokens');\n const box=$('authTokenInline');\n if(!box) return;\n // Note: Token lists stay inline in Users to keep user management fast and avoid nested modals.\n const tokens=j.tokens||[];\n box.classList.remove('d-none');\n box.innerHTML=`<div class=\"d-flex justify-content-between gap-2 flex-wrap mb-2\"><div><b><i class=\"fa-solid fa-shield-keyhole\"></i> API tokens</b><small>Active tokens for this user. Secrets are never shown after creation.</small></div><button id=\"authTokenInlineClose\" class=\"btn btn-xs btn-outline-secondary\" type=\"button\"><i class=\"fa-solid fa-xmark\"></i></button></div>${tokens.length ? tokens.map(t=>tokenRow(t,userId)).join('') : '<div class=\"empty-mini\">No API tokens.</div>'}`;\n $('authTokenInlineClose')?.addEventListener('click',()=>box.classList.add('d-none'));\n box.querySelectorAll('.auth-token-delete').forEach(btn=>btn.addEventListener('click',async()=>{ if(!confirm('Delete this API token?')) return; await deleteAuthToken(btn.dataset.userId, btn.dataset.tokenId); await showAuthTokens(btn.dataset.userId); }));\n }catch(e){ toast(e.message,'danger'); }\n }\n async function deleteAuthToken(userId, tokenId){\n // Note: Token revocation must fail loudly when the token is already gone.\n const j=await post(`/api/auth/users/${userId}/tokens/${tokenId}`, {}, 'DELETE');\n toast('API token deleted','success');\n await loadAuthUsers();\n return j;\n }\n function renderAuthProviderInfo(auth={}){\n const box=$('authProviderInfo');\n if(!box) return;\n const provider=auth.provider || window.PYTORRENT.authProvider || 'local';\n const isTinyAuth=provider==='tinyauth';\n const isExternal=!!auth.external || isTinyAuth || provider==='proxy';\n $('authPassword')?.classList.toggle('d-none', isTinyAuth);\n if(!isExternal){ box.classList.add('d-none'); box.innerHTML=''; return; }\n const permission=auth.auto_create_role==='admin' ? 'admin: all profiles with full access' : (auth.auto_create_permission==='none' ? 'user: no profile access' : `user: ${auth.auto_create_permission==='full'?'Full':'R/O'} on all profiles`);\n const lines=[\n `Authentication provider: ${provider==='tinyauth'?'TinyAuth':'proxy header'}`,\n auth.auto_create ? `Auto-created users get ${permission}.` : 'Automatic user creation is disabled.',\n auth.bypass_enabled ? `Auth bypass enabled for ${auth.bypass_hosts?.length ? auth.bypass_hosts.join(', ') : 'configured hosts'} as ${auth.bypass_user || 'admin'}.` : 'Auth bypass is disabled.',\n isTinyAuth ? 'TinyAuth manages passwords; pyTorrent password changes are hidden.' : ''\n ].filter(Boolean);\n box.classList.remove('d-none');\n box.innerHTML=`<div class=\"auth-provider-info-title\"><i class=\"fa-solid fa-shield-halved\"></i> External authentication</div><ul>${lines.map(line=>`<li>${esc(line)}</li>`).join('')}</ul>`;\n }\n async function loadAuthUsers(){\n if(!window.PYTORRENT.authEnabled || !$('authUsersManager')) return;\n const [usersRes, profilesRes]=await Promise.all([fetch('/api/auth/users'), fetch('/api/profiles')]);\n const usersJson=await usersRes.json();\n const profilesJson=await profilesRes.json();\n const profiles=profilesJson.profiles||[];\n const authInfo=usersJson.auth||{};\n renderAuthProviderInfo(authInfo);\n if($('authProfile')) $('authProfile').innerHTML=`<option value=\"0\">All profiles</option>`+profiles.map(p=>`<option value=\"${esc(p.id)}\">${esc(p.name)}</option>`).join('');\n const rows=(usersJson.users||[]).map(u=>{\n const perms=(u.permissions||[]).map(p=>`${p.profile_id?('profile '+p.profile_id):'all'}: ${p.access_level==='full'?'Full':'R/O'}`).join(', ') || (u.role==='admin'?'all: Full':'none');\n const tokenText=(u.api_tokens||0) ? `${u.api_tokens} active` : 'none';\n const actions=`<div class=\"auth-actions\"><button class=\"btn btn-xs btn-outline-secondary auth-edit\" data-user='${esc(JSON.stringify(u))}'><i class=\"fa-solid fa-pen\"></i><span>Edit</span></button><button class=\"btn btn-xs btn-outline-primary auth-token\" data-id=\"${esc(u.id)}\"><i class=\"fa-solid fa-key\"></i><span>Generate token</span></button><button class=\"btn btn-xs btn-outline-secondary auth-token-list\" data-id=\"${esc(u.id)}\"><i class=\"fa-solid fa-list\"></i><span>Tokens</span></button><button class=\"btn btn-xs btn-outline-danger auth-delete\" data-id=\"${esc(u.id)}\"><i class=\"fa-solid fa-trash-can\"></i><span>Remove</span></button></div>`;\n return [esc(u.username),esc(u.role),u.is_active?'yes':'no',esc(perms),`<button class=\"btn btn-xs btn-outline-secondary auth-token-list\" data-id=\"${esc(u.id)}\">${esc(tokenText)}</button>`,actions];\n });\n $('authUsersManager').innerHTML=rows.length?responsiveTable(['User','Role','Active','Profile rights','API tokens','Actions'],rows,'auth-users-table'):'<div class=\"empty-mini\">No users.</div>';\n }\n async function generateAuthToken(userId){\n const name=prompt('Token name', 'API token');\n if(name===null) return;\n try{\n const j=await post(`/api/auth/users/${userId}/tokens`, {name:name||'API token'});\n const token=j.token?.token||'';\n renderGeneratedToken(token);\n await copyText(token).then(()=>toast('API token copied','success')).catch(()=>toast('Copy the API token from the Users panel','warning'));\n await loadAuthUsers();\n }catch(e){ toast(e.message,'danger'); }\n }\n function resetAuthUserForm(){ ['authUserId','authUsername','authPassword'].forEach(id=>{ if($(id)) $(id).value=''; }); if($('authRole')) $('authRole').value='user'; if($('authProfile')) $('authProfile').value='0'; if($('authAccess')) $('authAccess').value='ro'; if($('authActive')) $('authActive').checked=true; $('authUserCancelBtn')?.classList.add('d-none'); }\n function editAuthUser(user){ if(!user) return; if($('authUserId')) $('authUserId').value=user.id||''; if($('authUsername')) $('authUsername').value=user.username||''; if($('authPassword')) $('authPassword').value=''; if($('authRole')) $('authRole').value=user.role||'user'; if($('authActive')) $('authActive').checked=!!user.is_active; const perm=(user.permissions||[])[0]||{profile_id:0,access_level:'ro'}; if($('authProfile')) $('authProfile').value=String(perm.profile_id||0); if($('authAccess')) $('authAccess').value=perm.access_level||'ro'; $('authUserCancelBtn')?.classList.remove('d-none'); }\n async function saveAuthUser(){\n const id=$('authUserId')?.value||'';\n const role=$('authRole')?.value||'user';\n const payload={username:$('authUsername')?.value||'',role,is_active:!!$('authActive')?.checked,permissions:role==='admin'?[]:[{profile_id:Number($('authProfile')?.value||0),access_level:$('authAccess')?.value||'ro'}]};\n if(!$('authPassword')?.classList.contains('d-none')) payload.password=$('authPassword')?.value||'';\n // Note: TinyAuth keeps passwords outside pyTorrent, so the hidden field is never submitted.\n try{ await post(id?`/api/auth/users/${id}`:'/api/auth/users',payload,id?'PUT':'POST'); toast('User saved','success'); resetAuthUserForm(); await loadAuthUsers(); }catch(e){ toast(e.message,'danger'); }\n }\n";
|