fix: sync with frontend and fix bugs

- Align API behavior with frontend integration
- Fix issues found during joint debugging

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
This commit is contained in:
GL.iNet-Yongping.Xie
2026-02-05 18:22:29 -08:00
parent 359f29484a
commit dff4d06107
11 changed files with 75 additions and 29 deletions
+10 -9
View File
@@ -9,13 +9,14 @@ const (
)
type Device struct {
ID int64
Ddns string
Mac string
Name string
Description string
IP string
DeviceGroupID *int64 // nil means ungrouped (admin-only visibility)
Status Status
LastSeenAt *int64
ID int64
Ddns string
Mac string
Name string
Description string
IP string
Client string
DeviceGroupID *int64 // nil means ungrouped (admin-only visibility)
Status Status
LastSeenAt *int64
}
+7 -7
View File
@@ -1,16 +1,16 @@
package dto
type Device struct {
ID int64 `json:"id"`
Ddns string `json:"ddns"`
ID int64 `json:"id"`
Ddns string `json:"ddns"`
Status string `json:"status"`
ConnectedTime int64 `json:"connectedTime"`
UpTime int64 `json:"upTime"`
IP string `json:"ip"`
Mac string `json:"mac"`
Description string `json:"description"`
DeviceGroupID *int64 `json:"deviceGroupId"`
DeviceGroupName string `json:"deviceGroupName"`
Mac string `json:"mac"`
Description string `json:"description"`
Client string `json:"client"`
DeviceGroupID *int64 `json:"deviceGroupId"`
DeviceGroupName string `json:"deviceGroupName"`
}
type ListDevicesResp struct {
+1 -3
View File
@@ -42,7 +42,6 @@ func (h *AuthHandler) Login(c *gin.Context) {
if authMethod == "ldap" {
ok, errorType := ldap.AuthenticateUserWithError(cfg, req.Username, req.Password, authMethod)
if !ok {
// Keep behavior consistent with /signin
if errorType == "authorization" {
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "User not authorized", nil))
} else {
@@ -55,8 +54,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
} else {
u, err := h.userSvc.Authenticate(c.Request.Context(), req.Username, req.Password)
if err != nil || u == nil {
// do not leak details
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "Permission denied", nil))
dto.Write(c, dto.Err(traceID, dto.CodeForbidden, "Authentication failed", nil))
return
}
userID = u.ID
+11 -8
View File
@@ -3,9 +3,9 @@ package handler
import (
"errors"
"io"
"sort"
"strconv"
"strings"
"time"
"rttys/internal/domain/device"
"rttys/internal/domain/identity"
@@ -118,7 +118,15 @@ func (h *DeviceHandler) ListDevices(c *gin.Context) {
}
}
now := time.Now().Unix()
sort.SliceStable(items, func(i, j int) bool {
oi := items[i].Status == device.StatusOnline
oj := items[j].Status == device.StatusOnline
if oi != oj {
return oi
}
return false
})
out := make([]dto.Device, 0, len(items))
for _, d := range items {
var groupName string
@@ -131,20 +139,15 @@ func (h *DeviceHandler) ListDevices(c *gin.Context) {
connectedTime = *d.LastSeenAt
}
var upTime int64
if d.LastSeenAt != nil && d.Status == device.StatusOnline && now >= *d.LastSeenAt {
upTime = now - *d.LastSeenAt
}
out = append(out, dto.Device{
ID: d.ID,
Ddns: d.Ddns,
Status: string(d.Status),
ConnectedTime: connectedTime,
UpTime: upTime,
IP: d.IP,
Mac: d.Mac,
Description: d.Description,
Client: d.Client,
DeviceGroupID: d.DeviceGroupID,
DeviceGroupName: groupName,
})
+5
View File
@@ -11,6 +11,11 @@ func SaveOrUpdateDeviceMeta(deviceID, mac, description, ip string) error {
return repo.SaveOrUpdate(context.Background(), deviceID, mac, description, ip)
}
func UpdateDeviceClient(deviceID, client string) error {
repo := sqlite.MustContainer().DeviceMeta
return repo.UpdateClient(context.Background(), deviceID, client)
}
func GetDeviceMetaByDeviceID(deviceID string) (*model.DeviceMeta, error) {
repo := sqlite.MustContainer().DeviceMeta
return repo.GetByDeviceID(context.Background(), deviceID)
+8 -1
View File
@@ -489,13 +489,20 @@ func handleDeviceInfoMsg(dev *Device, data []byte) error {
return nil
}
var payload map[string]any
var payload struct {
Client string `json:"client"`
}
if err := jsoniter.Unmarshal(data, &payload); err != nil {
log.Warn().Msgf("device '%s' client info invalid json: %v", dev.id, err)
return nil
}
dev.setClientInfo(data)
if payload.Client != "" {
if err := legacy.UpdateDeviceClient(dev.id, payload.Client); err != nil {
log.Warn().Err(err).Msgf("device '%s' update client info failed", dev.id)
}
}
log.Info().Msgf("device '%s' client info: %s", dev.id, string(data))
log.Debug().Msgf("device '%s' client info updated", dev.id)
return nil
+3
View File
@@ -20,6 +20,7 @@ type deviceRow struct {
Name string `gorm:"column:name"`
Description string `gorm:"column:description"`
IP string `gorm:"column:ip"`
Client string `gorm:"column:client"`
DeviceGroupID *int64 `gorm:"column:device_group_id"` // NULL => nil
Status string `gorm:"column:status"`
LastSeenAt *int64 `gorm:"column:last_seen_at"` // NULL => nil
@@ -45,6 +46,7 @@ func (r *DeviceRepo) ListAll(ctx context.Context) ([]device.Device, error) {
Name: row.Name,
Description: row.Description,
IP: row.IP,
Client: row.Client,
DeviceGroupID: row.DeviceGroupID,
Status: device.Status(row.Status),
LastSeenAt: row.LastSeenAt,
@@ -76,6 +78,7 @@ func (r *DeviceRepo) ListByDeviceGroupIDs(ctx context.Context, groupIDs []int64)
Name: row.Name,
Description: row.Description,
IP: row.IP,
Client: row.Client,
DeviceGroupID: row.DeviceGroupID,
Status: device.Status(row.Status),
LastSeenAt: row.LastSeenAt,
+10
View File
@@ -38,6 +38,16 @@ func (r *DeviceMetaRepo) SaveOrUpdate(ctx context.Context, deviceID, mac, descri
).Error
}
func (r *DeviceMetaRepo) UpdateClient(ctx context.Context, deviceID, client string) error {
if r.db == nil {
return fmt.Errorf("gorm db is nil")
}
return r.db.WithContext(ctx).Exec(
`UPDATE devices SET client=? WHERE ddns=?`,
client, deviceID,
).Error
}
func (r *DeviceMetaRepo) GetByDeviceID(ctx context.Context, deviceID string) (*model.DeviceMeta, error) {
var meta model.DeviceMeta
err := r.db.WithContext(ctx).Where("ddns = ?", deviceID).First(&meta).Error
+1
View File
@@ -58,6 +58,7 @@ CREATE TABLE IF NOT EXISTS devices (
name TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT '',
client TEXT NOT NULL DEFAULT '',
device_group_id INTEGER NULL,
status TEXT NOT NULL DEFAULT 'online' CHECK (status IN ('online','offline','disabled')),
last_seen_at INTEGER NULL,
+18 -1
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"os"
"strings"
gormsqlite "github.com/glebarez/sqlite"
"gorm.io/gorm"
@@ -74,6 +75,22 @@ func InitSchema(ctx context.Context, db *sql.DB, schemaPath string) error {
if err != nil {
return err
}
_, err = db.ExecContext(ctx, string(b))
if _, err = db.ExecContext(ctx, string(b)); err != nil {
return err
}
return ensureDeviceClientColumn(ctx, db)
}
func ensureDeviceClientColumn(ctx context.Context, db *sql.DB) error {
if db == nil {
return nil
}
_, err := db.ExecContext(ctx, `ALTER TABLE devices ADD COLUMN client TEXT NOT NULL DEFAULT ''`)
if err == nil {
return nil
}
if strings.Contains(err.Error(), "duplicate column name") {
return nil
}
return err
}
+1
View File
@@ -6,6 +6,7 @@ type DeviceMeta struct {
Mac string `gorm:"column:mac"` // Mac is the unique and immutable MAC address of the device.
IP string `gorm:"column:ip"` // IP is the current IP address of the device.
Description string `gorm:"column:description"` // Description is a human-readable description of the device.
Client string `gorm:"column:client"` // Client reported by device (e.g. "rtty-go").
}
// TableName sets the name of the table in the database that this struct binds to.