120ae186ef
- 将11个业务页面(index/servers/files/scripts/credentials/settings/terminal等) 的内联侧边栏全部迁移到layout.js的initLayout()共享组件 - 移除各页面重复的loadUser()函数,统一由layout.js处理 - 统一全局搜索功能、用户信息加载、品牌名称显示 - 消除约800行重复的侧边栏HTML代码 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
12 KiB
HTML
169 lines
12 KiB
HTML
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 凭据管理</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script><style type="text/tailwindcss">@theme{--color-brand:oklch(55% 0.2 250);--color-brand-light:oklch(75% 0.15 250);--color-brand-dark:oklch(35% 0.18 250)}</style></head>
|
|
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true,tab:'db'}">
|
|
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0" data-sidebar></aside>
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
<header class="bg-slate-900 border-b border-slate-800 px-6 py-3 flex items-center justify-between">
|
|
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">凭据管理</h1></div>
|
|
<button onclick="showAddCred()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">+ 新建</button>
|
|
</header>
|
|
<!-- Tabs -->
|
|
<div class="bg-slate-900 border-b border-slate-800 px-6 flex">
|
|
<button onclick="switchTab('db')" id="tabDB" class="px-4 py-3 text-sm border-b-2 border-brand text-brand-light transition">数据库凭据</button>
|
|
<button onclick="switchTab('preset')" id="tabPreset" class="px-4 py-3 text-sm border-b-2 border-transparent text-slate-400 hover:text-white transition">密码预设</button>
|
|
</div>
|
|
<main class="flex-1 overflow-y-auto p-6">
|
|
<!-- DB Credentials -->
|
|
<div id="dbSection">
|
|
<div id="credsList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
|
|
</div>
|
|
<!-- Password Presets -->
|
|
<div id="presetSection" class="hidden">
|
|
<div id="presetsList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
|
|
</div>
|
|
|
|
<!-- Add DB Credential Modal -->
|
|
<div id="credModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md space-y-3">
|
|
<h2 class="text-white font-semibold text-lg">新建数据库凭据</h2>
|
|
<div><label class="block text-slate-400 text-xs mb-1">名称</label><input id="credName" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="prod-mysql"></div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div><label class="block text-slate-400 text-xs mb-1">类型</label><select id="credType" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><option value="mysql">MySQL</option><option value="postgresql">PostgreSQL</option><option value="mariadb">MariaDB</option><option value="mongodb">MongoDB</option><option value="redis">Redis</option></select></div>
|
|
<div><label class="block text-slate-400 text-xs mb-1">端口</label><input id="credPort" type="number" value="3306" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
|
|
</div>
|
|
<div><label class="block text-slate-400 text-xs mb-1">主机</label><input id="credHost" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="192.168.1.10"></div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div><label class="block text-slate-400 text-xs mb-1">用户名</label><input id="credUser" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="root"></div>
|
|
<div><label class="block text-slate-400 text-xs mb-1">密码</label><input id="credPass" type="password" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
|
|
</div>
|
|
<div><label class="block text-slate-400 text-xs mb-1">数据库</label><input id="credDb" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="可选"></div>
|
|
<div class="flex gap-2 pt-2"><button onclick="createCred()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideCredModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Password Preset Modal -->
|
|
<div id="presetModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md space-y-3">
|
|
<h2 class="text-white font-semibold text-lg">新建密码预设</h2>
|
|
<div><label class="block text-slate-400 text-xs mb-1">名称</label><input id="presetName" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="生产环境root密码"></div>
|
|
<div><label class="block text-slate-400 text-xs mb-1">密码</label><input id="presetPass" type="password" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="输入密码(将加密存储)"></div>
|
|
<div class="bg-slate-800/50 rounded-lg p-3 text-xs text-slate-500">密码将使用 AES-256 加密后存储,不可逆向查看。推送时自动解密使用。</div>
|
|
<div class="flex gap-2 pt-2"><button onclick="createPreset()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hidePresetModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<script src="/app/api.js"></script>
|
|
<script src="/app/layout.js"></script>
|
|
<script>
|
|
initLayout('credentials');
|
|
const DB_ICONS={mysql:'🐬',postgresql:'🐘',mariadb:'🐬',mongodb:'🍃',redis:'🔴'};
|
|
const DB_PORTS={mysql:3306,postgresql:5432,mariadb:3306,mongodb:27017,redis:6379};
|
|
|
|
function switchTab(t){
|
|
document.getElementById('tabDB').className='px-4 py-3 text-sm border-b-2 '+(t==='db'?'border-brand text-brand-light':'border-transparent text-slate-400 hover:text-white')+' transition';
|
|
document.getElementById('tabPreset').className='px-4 py-3 text-sm border-b-2 '+(t==='preset'?'border-brand text-brand-light':'border-transparent text-slate-400 hover:text-white')+' transition';
|
|
document.getElementById('dbSection').classList.toggle('hidden',t!=='db');
|
|
document.getElementById('presetSection').classList.toggle('hidden',t!=='preset');
|
|
if(t==='preset')loadPresets();
|
|
}
|
|
|
|
// ── DB Credentials ──
|
|
async function loadCreds(){
|
|
try{
|
|
const r=await apiFetch(API+'/scripts/credentials');if(!r)return;
|
|
const creds=await r.json();
|
|
document.getElementById('credsList').innerHTML=creds.length?creds.map(c=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-lg">${DB_ICONS[c.db_type]||'🗄'}</span>
|
|
<div>
|
|
<span class="text-white font-medium">${esc(c.name)}</span>
|
|
<div class="text-slate-500 text-xs mt-0.5">${esc(c.db_type)} · ${esc(c.host)}:${c.port} · ${esc(c.username)}${c.database?' / '+esc(c.database):''}</div>
|
|
</div>
|
|
</div>
|
|
<button onclick="deleteCred(${c.id})" class="text-red-400 text-xs hover:underline">删除</button>
|
|
</div>
|
|
</div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无数据库凭据</div>';
|
|
}catch(e){document.getElementById('credsList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
|
|
}
|
|
|
|
function showAddCred(){
|
|
const t=Alpine?Alpine.store?'db':document.querySelector('[x-data]')?.__x?.$data?.tab:'db':'db';
|
|
// Check which tab is active
|
|
if(!document.getElementById('dbSection').classList.contains('hidden')){
|
|
['credName','credHost','credUser','credPass','credDb'].forEach(id=>document.getElementById(id).value='');
|
|
document.getElementById('credType').value='mysql';
|
|
document.getElementById('credPort').value='3306';
|
|
document.getElementById('credModal').classList.remove('hidden');
|
|
}else{
|
|
document.getElementById('presetName').value='';
|
|
document.getElementById('presetPass').value='';
|
|
document.getElementById('presetModal').classList.remove('hidden');
|
|
}
|
|
}
|
|
function hideCredModal(){document.getElementById('credModal').classList.add('hidden')}
|
|
function hidePresetModal(){document.getElementById('presetModal').classList.add('hidden')}
|
|
|
|
document.getElementById('credType').addEventListener('change',()=>{
|
|
const t=document.getElementById('credType').value;
|
|
document.getElementById('credPort').value=DB_PORTS[t]||3306;
|
|
});
|
|
|
|
async function createCred(){
|
|
const body={name:document.getElementById('credName').value,db_type:document.getElementById('credType').value,host:document.getElementById('credHost').value,port:parseInt(document.getElementById('credPort').value)||3306,username:document.getElementById('credUser').value,password:document.getElementById('credPass').value,database:document.getElementById('credDb').value||null};
|
|
if(!body.name||!body.host||!body.username||!body.password){toast('warning','名称、主机、用户名、密码必填');return}
|
|
const r=await apiFetch(API+'/scripts/credentials',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});
|
|
if(r&&r.ok)toast('success','凭据已创建');else toast('error','创建失败');
|
|
hideCredModal();loadCreds();
|
|
}
|
|
|
|
async function deleteCred(id){
|
|
if(!confirm('确定删除凭据?'))return;
|
|
const r=await apiFetch(API+'/scripts/credentials/'+id,{method:'DELETE'});
|
|
if(r&&r.ok)toast('success','凭据已删除');else toast('error','删除失败');
|
|
loadCreds();
|
|
}
|
|
|
|
// ── Password Presets ──
|
|
async function loadPresets(){
|
|
try{
|
|
const r=await apiFetch(API+'/presets/');if(!r)return;
|
|
const presets=await r.json();
|
|
document.getElementById('presetsList').innerHTML=presets.length?presets.map(p=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-lg">🔑</span>
|
|
<div>
|
|
<span class="text-white font-medium">${esc(p.name)}</span>
|
|
<div class="text-slate-500 text-xs mt-0.5">加密存储 · 创建于 ${fmtTime(p.created_at)}</div>
|
|
</div>
|
|
</div>
|
|
<button onclick="deletePreset(${p.id})" class="text-red-400 text-xs hover:underline">删除</button>
|
|
</div>
|
|
</div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无密码预设</div>';
|
|
}catch(e){document.getElementById('presetsList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
|
|
}
|
|
|
|
async function createPreset(){
|
|
const name=document.getElementById('presetName').value;
|
|
const pw=document.getElementById('presetPass').value;
|
|
if(!name||!pw){toast('warning','名称和密码必填');return}
|
|
const r=await apiFetch(API+'/presets/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({name,encrypted_pw:pw})});
|
|
if(r&&r.ok)toast('success','密码预设已创建');else toast('error','创建失败');
|
|
hidePresetModal();loadPresets();
|
|
}
|
|
|
|
async function deletePreset(id){
|
|
if(!confirm('确定删除密码预设?'))return;
|
|
const r=await apiFetch(API+'/presets/'+id,{method:'DELETE'});
|
|
if(r&&r.ok)toast('success','预设已删除');else toast('error','删除失败');
|
|
loadPresets();
|
|
}
|
|
|
|
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
|
function fmtTime(t){if(!t)return'--';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
|
|
|
|
loadCreds();
|
|
</script>
|
|
</body></html>
|