diff --git a/internal/domain/device/model.go b/internal/domain/device/model.go index fd2d825..2c967af 100755 --- a/internal/domain/device/model.go +++ b/internal/domain/device/model.go @@ -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 } diff --git a/internal/http/dto/device.go b/internal/http/dto/device.go index 827bb7f..d464f26 100755 --- a/internal/http/dto/device.go +++ b/internal/http/dto/device.go @@ -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 { diff --git a/internal/http/handler/auth.go b/internal/http/handler/auth.go index c42cede..f237570 100755 --- a/internal/http/handler/auth.go +++ b/internal/http/handler/auth.go @@ -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 diff --git a/internal/http/handler/device.go b/internal/http/handler/device.go index 1759ef6..0d3aed4 100755 --- a/internal/http/handler/device.go +++ b/internal/http/handler/device.go @@ -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, }) diff --git a/internal/legacy/devicemeta_compat.go b/internal/legacy/devicemeta_compat.go index fcdd227..c500d73 100755 --- a/internal/legacy/devicemeta_compat.go +++ b/internal/legacy/devicemeta_compat.go @@ -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) diff --git a/internal/server/device.go b/internal/server/device.go index 00c397f..c20c64b 100644 --- a/internal/server/device.go +++ b/internal/server/device.go @@ -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 diff --git a/internal/store/sqlite/device_repo.go b/internal/store/sqlite/device_repo.go index 382e31e..b6a69db 100755 --- a/internal/store/sqlite/device_repo.go +++ b/internal/store/sqlite/device_repo.go @@ -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, diff --git a/internal/store/sqlite/devicemeta_repo.go b/internal/store/sqlite/devicemeta_repo.go index 8ab80e9..26b2fa1 100755 --- a/internal/store/sqlite/devicemeta_repo.go +++ b/internal/store/sqlite/devicemeta_repo.go @@ -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 diff --git a/internal/store/sqlite/schema.sql b/internal/store/sqlite/schema.sql index 2e5e71f..403052a 100755 --- a/internal/store/sqlite/schema.sql +++ b/internal/store/sqlite/schema.sql @@ -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, diff --git a/internal/store/sqlite/sqlite.go b/internal/store/sqlite/sqlite.go index 6149e76..dc17f25 100755 --- a/internal/store/sqlite/sqlite.go +++ b/internal/store/sqlite/sqlite.go @@ -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 } diff --git a/model/model.go b/model/model.go index 9f519bf..d23cf64 100755 --- a/model/model.go +++ b/model/model.go @@ -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.