fix: protect device register with admin auth; improve admin UI (full API key, copy button, styling)
This commit is contained in:
parent
e828ebd44e
commit
5db3da3618
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -82,15 +82,15 @@ API-ключ — это токен, который клиент (Верстак
|
|||
2. В разделе "API Keys" ввести имя устройства и нажать "Create".
|
||||
3. Скопировать сгенерированный ключ.
|
||||
|
||||
Через API (без авторизации):
|
||||
Через API (требует логин и пароль администратора):
|
||||
```bash
|
||||
curl -X POST http://localhost:47732/api/v1/device/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"мой-ноутбук"}'
|
||||
-d '{"name":"мой-ноутбук","username":"admin","password":"пароль-админа"}'
|
||||
```
|
||||
Ответ: `{"device_id":"...","api_key":"..."}`
|
||||
|
||||
**Важно:** endpoint регистрации открыт без аутентификации. Не выставляйте сервер в интернет без дополнительной защиты (фаервол, VPN, reverse proxy с базовой аутентификацией).
|
||||
**Важно:** не выставляйте сервер в интернет без HTTPS (через reverse proxy). До создания полноценной системы пользователей регистрация устройств требует учётных данных администратора.
|
||||
|
||||
### Как использовать
|
||||
|
||||
|
|
@ -158,7 +158,6 @@ sync:
|
|||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/v1/health` | Проверка здоровья сервера |
|
||||
| POST | `/api/v1/device/register` | Регистрация устройства (без аутентификации) |
|
||||
|
||||
### Требуют API-ключ (Authorization: Bearer)
|
||||
|
||||
|
|
@ -169,6 +168,12 @@ sync:
|
|||
| POST | `/api/v1/blobs/` | Загрузить blob (multipart) |
|
||||
| GET | `/api/v1/blobs/{sha256}` | Скачать blob |
|
||||
|
||||
### Требуют логин+пароль администратора
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| POST | `/api/v1/device/register` | Регистрация устройства (body: name + username + password) |
|
||||
|
||||
### Требуют сессию админа (cookie)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ curl -sf "http://localhost:$SERVER_PORT/api/v1/health" | grep -q '"ok"' && echo
|
|||
|
||||
echo ":: Register device"
|
||||
REG=$(curl -sf -X POST "http://localhost:$SERVER_PORT/api/v1/device/register" \
|
||||
-H "Content-Type: application/json" -d '{"name":"smoke"}')
|
||||
-H "Content-Type: application/json" -d '{"name":"smoke","username":"admin","password":"pass"}')
|
||||
DID=$(echo "$REG" | python3 -c "import sys,json;print(json.load(sys.stdin)['device_id'])")
|
||||
AKEY=$(echo "$REG" | python3 -c "import sys,json;print(json.load(sys.stdin)['api_key'])")
|
||||
echo " device=$DID"
|
||||
|
|
|
|||
Loading…
Reference in New Issue