fix: protect device register with admin auth; improve admin UI (full API key, copy button, styling)

This commit is contained in:
2026-06-01 23:22:19 +08:00
parent e828ebd44e
commit 5db3da3618
3 changed files with 56 additions and 14 deletions
+46 -9
View File
@@ -320,7 +320,9 @@ func (s *Server) handleDeviceRegister(w http.ResponseWriter, r *http.Request) {
return
}
var req struct {
Name string `json:"name"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonErr(w, 400, "invalid JSON")
@@ -330,6 +332,14 @@ func (s *Server) handleDeviceRegister(w http.ResponseWriter, r *http.Request) {
jsonErr(w, 400, "name required")
return
}
if req.Username == "" || req.Password == "" {
jsonErr(w, 401, "admin username and password required")
return
}
if !s.cfg.CheckAdmin(req.Username, req.Password) {
jsonErr(w, 401, "invalid admin credentials")
return
}
b := make([]byte, 20)
rand.Read(b)
@@ -337,7 +347,7 @@ func (s *Server) handleDeviceRegister(w http.ResponseWriter, r *http.Request) {
now := time.Now().UTC().Format(time.RFC3339)
result, err := s.db.Exec(
_, err := s.db.Exec(
"INSERT INTO server_devices (id, name, api_key, last_seen, created_at) VALUES (?, ?, ?, ?, ?)",
apiKey[:12], req.Name, apiKey, now, now,
)
@@ -345,8 +355,6 @@ func (s *Server) handleDeviceRegister(w http.ResponseWriter, r *http.Request) {
jsonErr(w, 500, err.Error())
return
}
id, _ := result.LastInsertId()
_ = id
jsonOK(w, map[string]interface{}{
"device_id": apiKey[:12],
@@ -587,7 +595,29 @@ func (s *Server) handleAdminDashboard(w http.ResponseWriter, r *http.Request) {
<html lang="ru">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Verstak Sync — Admin</title>
<style>body{font-family:sans-serif;background:#13131f;color:#e4e4ef;padding:24px;max-width:800px;margin:0 auto}a{color:#6366f1}h1{border-bottom:1px solid #2a2a3c;padding-bottom:12px}.stat{background:#1a1a28;border:1px solid #2a2a3c;padding:12px 16px;border-radius:8px;margin:8px 0}.row{display:flex;gap:16px}</style>
<style>
body{font-family:sans-serif;background:#13131f;color:#e4e4ef;padding:24px;max-width:800px;margin:0 auto}
a{color:#6366f1}
h1{border-bottom:1px solid #2a2a3c;padding-bottom:12px}
h2{margin-top:24px}
.stat{background:#1a1a28;border:1px solid #2a2a3c;padding:12px 16px;border-radius:8px;margin:8px 0}
.row{display:flex;gap:16px}
table{width:100%;border-collapse:collapse;margin-top:8px}
th,td{text-align:left;padding:8px 12px;border-bottom:1px solid #2a2a3c}
th{font-size:12px;color:#888;text-transform:uppercase}
.key-cell{max-width:360px;overflow:hidden;text-overflow:ellipsis;font-family:monospace;font-size:12px;color:#b0b0c0}
.btn{font-family:inherit;font-size:12px;padding:6px 12px;border-radius:6px;border:1px solid #2a2a3c;background:#1a1a28;color:#ccc;cursor:pointer}
.btn:hover{background:#222233}
.btn-primary{background:#6366f1;border-color:#6366f1;color:#fff}
.btn-primary:hover{background:#4f46e5}
.btn-danger{color:#ff6b6b;border-color:#4a2222}
.btn-danger:hover{background:#3a2222}
.copy-btn{padding:2px 8px;font-size:11px;margin-left:6px}
input{font-family:inherit;font-size:14px;padding:8px 12px;border:1px solid #2a2a3c;background:#13131f;color:#e4e4ef;border-radius:6px;margin-right:8px}
input:focus{outline:none;border-color:#6366f1}
form{margin-top:8px}
.copied-msg{color:#4ade80;font-size:12px;margin-left:6px}
</style>
</head><body>
<h1>Verstak Sync Server</h1>
<div class="row">
@@ -600,16 +630,23 @@ func (s *Server) handleAdminDashboard(w http.ResponseWriter, r *http.Request) {
fetch('/admin/api/keys').then(r=>r.json()).then(keys=>{
const div=document.getElementById('keys')
if(!keys.length){div.innerHTML='<p>Нет ключей</p>';return}
div.innerHTML='<table><tr><th>Устройство</th><th>Ключ</th><th></th></tr>'+
keys.map(k=>'<tr><td>'+k.name+'</td><td><code>'+k.api_key.slice(0,16)+'…</code></td>'+
'<td><button onclick="delKey(\''+k.id+'\')">Удалить</button></td></tr>').join('')+'</table>'
div.innerHTML='<table><tr><th>Устройство</th><th>API-ключ</th><th></th><th></th></tr>'+
keys.map(k=>'<tr><td>'+k.name+'</td><td class="key-cell" title="'+k.api_key+'">'+k.api_key+'</td>'+
'<td><button class="btn copy-btn" onclick="copyKey(\''+k.api_key+'\',this)">Копировать</button></td>'+
'<td><button class="btn btn-danger" onclick="delKey(\''+k.id+'\')">Удалить</button></td></tr>').join('')+'</table>'
})
function copyKey(key,btn){
navigator.clipboard.writeText(key).then(()=>{
var old=btn.textContent;btn.textContent='Скопировано';btn.style.color='#4ade80'
setTimeout(function(){btn.textContent=old;btn.style.color=''},1500)
})
}
function delKey(id){if(confirm('Удалить ключ?'))fetch('/admin/api/keys/'+id,{method:'DELETE'}).then(()=>location.reload())}
</script>
<h2>Новый ключ</h2>
<form action="/admin/api/keys" method="POST">
<input name="name" placeholder="Название устройства" required>
<button>Создать</button>
<button class="btn btn-primary">Создать</button>
</form>
<p><a href="/api/v1/health">Health check</a></p>
</body></html>`, deviceCount, opsCount)