From 5db3da361833cd1bfcc5c71e129ed2c09ba46559 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Mon, 1 Jun 2026 23:22:19 +0800 Subject: [PATCH] fix: protect device register with admin auth; improve admin UI (full API key, copy button, styling) --- cmd/verstak-server/server.go | 55 ++++++++++++++++++++++++++++++------ docs/10_Sync_Server_Guide.md | 13 ++++++--- test_smoke_sync.sh | 2 +- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/cmd/verstak-server/server.go b/cmd/verstak-server/server.go index cb47ea6..26c4088 100644 --- a/cmd/verstak-server/server.go +++ b/cmd/verstak-server/server.go @@ -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) { Verstak Sync — Admin - +

Verstak Sync Server

@@ -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='

Нет ключей

';return} - div.innerHTML=''+ - keys.map(k=>''+ - '').join('')+'
УстройствоКлюч
'+k.name+''+k.api_key.slice(0,16)+'…
' + div.innerHTML=''+ + keys.map(k=>''+ + ''+ + '').join('')+'
УстройствоAPI-ключ
'+k.name+''+k.api_key+'
' }) +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())}

Новый ключ

- +

Health check

`, deviceCount, opsCount) diff --git a/docs/10_Sync_Server_Guide.md b/docs/10_Sync_Server_Guide.md index 46f26b7..7f8efbf 100644 --- a/docs/10_Sync_Server_Guide.md +++ b/docs/10_Sync_Server_Guide.md @@ -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) | Метод | Путь | Описание | diff --git a/test_smoke_sync.sh b/test_smoke_sync.sh index ef91af9..888acde 100755 --- a/test_smoke_sync.sh +++ b/test_smoke_sync.sh @@ -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"