ssh_keys.html 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>SSH 密钥管理 - PlateRecApp</title>
  7. <style>
  8. *{margin:0;padding:0;box-sizing:border-box}
  9. body{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
  10. .header{background:#1e293b;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155}
  11. .header h1{font-size:20px;font-weight:600;color:#f8fafc}
  12. .nav{display:flex;gap:16px;align-items:center}
  13. .nav a{color:#60a5fa;text-decoration:none;font-size:14px;padding:6px 12px;border-radius:6px}
  14. .nav a:hover,.nav a.active{background:#334155}
  15. .container{max-width:900px;margin:0 auto;padding:20px}
  16. .card{background:#1e293b;border-radius:12px;padding:20px;border:1px solid #334155;margin-bottom:16px}
  17. .card-title{font-size:15px;font-weight:600;margin-bottom:12px;color:#f8fafc}
  18. table{width:100%;border-collapse:collapse;font-size:14px}
  19. th{text-align:left;padding:8px 12px;color:#94a3b8;border-bottom:1px solid #334155;font-weight:500}
  20. td{padding:8px 12px;border-bottom:1px solid #1e293b}
  21. .key-type{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500}
  22. .key-type.rsa{background:#854d0e;color:#fde047}
  23. .key-type.ed25519{background:#1e3a5f;color:#7dd3fc}
  24. .key-type.other{background:#374151;color:#9ca3af}
  25. .btn{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s}
  26. .btn-primary{background:#3b82f6;color:#fff}.btn-primary:hover{background:#2563eb}
  27. .btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c}
  28. .btn-sm{padding:4px 10px;font-size:12px}
  29. textarea{width:100%;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:12px;font-family:'Fira Code',monospace;font-size:13px;resize:vertical;min-height:80px}
  30. textarea:focus{outline:none;border-color:#3b82f6}
  31. .msg{padding:10px 14px;border-radius:8px;margin-top:10px;font-size:14px;display:none}
  32. .msg.success{display:block;background:#14532d;color:#86efac;border:1px solid #16a34a}
  33. .msg.error{display:block;background:#7f1d1d;color:#fca5a5;border:1px solid #dc2626}
  34. .empty{text-align:center;color:#64748b;padding:20px}
  35. @media (max-width: 768px) {
  36. .header { flex-direction: column; gap: 8px; padding: 12px 16px; }
  37. .header h1 { font-size: 16px; }
  38. .nav { flex-wrap: wrap; gap: 8px; justify-content: center; }
  39. .nav a { font-size: 12px; padding: 4px 8px; }
  40. .container { padding: 12px; }
  41. .card { padding: 14px; }
  42. table { font-size: 12px; display: block; overflow-x: auto; white-space: nowrap; }
  43. th, td { padding: 6px 8px; }
  44. .btn-group { flex-wrap: wrap; }
  45. .btn { flex: 1; min-width: 80px; text-align: center; font-size: 12px; }
  46. textarea { min-height: 60px; font-size: 12px; }
  47. }
  48. @media (max-width: 480px) {
  49. .card-title { font-size: 13px; }
  50. }
  51. </style>
  52. </head>
  53. <body>
  54. <div class="header">
  55. <h1>🔑 SSH 密钥管理</h1>
  56. <div class="nav">
  57. <a href="/">监控台</a>
  58. <a href="/video">视频</a>
  59. <a href="/config">配置</a>
  60. <a href="/system">系统</a>
  61. <a href="/frpc">frpc</a>
  62. <a href="/ssh-keys" class="active">SSH</a>
  63. <a href="/logs">日志</a>
  64. <a href="/help">帮助</a>
  65. </div>
  66. </div>
  67. <div class="container">
  68. <div class="card">
  69. <div class="card-title">已授权公钥</div>
  70. <table>
  71. <thead><tr><th>#</th><th>类型</th><th>注释</th><th>密钥预览</th><th>操作</th></tr></thead>
  72. <tbody id="key-list"><tr><td colspan="5" class="empty">加载中...</td></tr></tbody>
  73. </table>
  74. </div>
  75. <div class="card">
  76. <div class="card-title">添加公钥</div>
  77. <textarea id="new-key" placeholder="粘贴 SSH 公钥,例如:&#10;ssh-rsa AAAAB3NzaC1yc2EAAA... user@host&#10;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@host"></textarea>
  78. <div style="margin-top:10px">
  79. <button class="btn btn-primary" onclick="addKey()">➕ 添加公钥</button>
  80. <button class="btn btn-primary" onclick="loadKeys()" style="margin-left:8px">🔄 刷新列表</button>
  81. </div>
  82. <div id="msg" class="msg"></div>
  83. </div>
  84. </div>
  85. <script>
  86. function getCookie(n){var m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}function authFetch(u,o){o=o||{};var h=o.headers||{};if(!(h instanceof Headers)){var nh=new Headers();for(var k in h)nh.set(k,h[k]);h=nh;}if(!h.has("X-CSRF-Token")){var t=getCookie("csrf_token");if(t)h.set("X-CSRF-Token",t);}o.headers=h;return fetch(u,o).then(function(r){if(r.status===401){window.location.href="/login?redirect="+encodeURIComponent(window.location.pathname);return new Promise(function(){});}return r;});}
  87. function showMsg(text,ok){
  88. let el=document.getElementById('msg');
  89. el.className='msg '+(ok?'success':'error');
  90. el.textContent=text;
  91. setTimeout(()=>el.className='msg',5000);
  92. }
  93. function getKeyTypeClass(type){
  94. if(type.includes('ed25519'))return'ed25519';
  95. if(type.includes('rsa'))return'rsa';
  96. return'other';
  97. }
  98. function loadKeys(){
  99. authFetch('/api/ssh/keys').then(r=>r.json()).then(d=>{
  100. let tbody=document.getElementById('key-list');
  101. if(!d.keys||d.keys.length===0){
  102. tbody.innerHTML='<tr><td colspan="5" class="empty">暂无授权公钥</td></tr>';
  103. return;
  104. }
  105. tbody.innerHTML=d.keys.map((k,i)=>'<tr><td>'+(i+1)+'</td><td><span class="key-type '+getKeyTypeClass(k.type)+'">'+k.type+'</span></td><td>'+k.comment+'</td><td style="font-family:monospace;font-size:12px;color:#64748b">'+k.key_preview+'</td><td><button class="btn btn-danger btn-sm" onclick="deleteKey('+k.index+')">删除</button></td></tr>').join('');
  106. });
  107. }
  108. function addKey(){
  109. let key=document.getElementById('new-key').value.trim();
  110. if(!key){showMsg('请输入公钥',false);return}
  111. authFetch('/api/ssh/keys',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:key})})
  112. .then(r=>r.json()).then(d=>{
  113. showMsg(d.message||d.error,d.success);
  114. if(d.success){document.getElementById('new-key').value='';loadKeys()}
  115. });
  116. }
  117. function deleteKey(index){
  118. if(!confirm('确定要删除第 '+(index+1)+' 条公钥吗?此操作不可撤销!')) return;
  119. authFetch('/api/ssh/keys',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({index:index})})
  120. .then(r=>r.json()).then(d=>{
  121. showMsg(d.message||d.error,d.success);
  122. if(d.success) loadKeys();
  123. });
  124. }
  125. loadKeys();
  126. </script>
  127. </body>
  128. </html>