228 lines
8.6 KiB
Go
228 lines
8.6 KiB
Go
package version
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"git.ilapage.cn/ila/yovision/Sense/server/app/sense/credential"
|
|
deviceModels "git.ilapage.cn/ila/yovision/Sense/server/app/sense/device/models"
|
|
"git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration"
|
|
migrationModels "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration/models"
|
|
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
|
|
)
|
|
|
|
type deviceCasbinRule struct {
|
|
ID uint `gorm:"primaryKey;autoIncrement"`
|
|
Ptype string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
V0 string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
V1 string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
V2 string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
V3 string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
V4 string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
V5 string `gorm:"size:512;uniqueIndex:unique_index"`
|
|
}
|
|
|
|
func (deviceCasbinRule) TableName() string { return "casbin_rule" }
|
|
|
|
func init() {
|
|
_, fileName, _, _ := runtime.Caller(0)
|
|
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateSenseDeviceLedger)
|
|
}
|
|
|
|
func migrateSenseDeviceLedger(db *gorm.DB, version string) error {
|
|
return db.Transaction(func(tx *gorm.DB) error {
|
|
if err := prepareLegacyDeviceCapabilities(tx); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.AutoMigrate(&deviceModels.Device{}, &credential.DeviceCredential{}, &deviceCasbinRule{}); err != nil {
|
|
return err
|
|
}
|
|
page, err := ensureDeviceMenu(tx, migrationModels.SysMenu{
|
|
MenuName: "SenseDeviceManage", Title: "设备管理", Icon: "monitor", Path: "/sense/devices",
|
|
MenuType: "C", Permission: "sense:device:list", ParentId: 0, Component: "/sense/device/index",
|
|
Sort: 5, Visible: "0", IsFrame: "1",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buttons := make([]migrationModels.SysMenu, 0, 4)
|
|
for index, definition := range []struct{ name, title, action, permission string }{
|
|
{"SenseDeviceAdd", "新增设备", "POST", "sense:device:add"},
|
|
{"SenseDeviceEdit", "编辑设备", "PUT", "sense:device:edit"},
|
|
{"SenseDeviceDisable", "停用设备", "PUT", "sense:device:disable"},
|
|
{"SenseDeviceCredential", "更新设备凭据", "PUT", "sense:device:credential"},
|
|
} {
|
|
button, err := ensureDeviceMenu(tx, migrationModels.SysMenu{
|
|
MenuName: definition.name, Title: definition.title, MenuType: "F", Action: definition.action,
|
|
Permission: definition.permission, ParentId: page.MenuId, Paths: fmt.Sprintf("/0/%d", page.MenuId),
|
|
Sort: index + 1, Visible: "1", IsFrame: "1",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buttons = append(buttons, button)
|
|
}
|
|
if err = attachDeviceRole(tx, "implementation_operator", append([]migrationModels.SysMenu{page}, buttons...)); err != nil {
|
|
return err
|
|
}
|
|
if err = attachDeviceRole(tx, "site_admin", append([]migrationModels.SysMenu{page}, buttons...)); err != nil {
|
|
return err
|
|
}
|
|
if err = attachDeviceRole(tx, "viewer", []migrationModels.SysMenu{page}); err != nil {
|
|
return err
|
|
}
|
|
readPolicies := [][2]string{{"/api/v1/devices", "GET"}, {"/api/v1/devices/:id", "GET"}}
|
|
writePolicies := [][2]string{{"/api/v1/devices", "POST"}, {"/api/v1/devices/:id", "PUT"}, {"/api/v1/devices/:id/disable", "PUT"}, {"/api/v1/devices/:id/credentials", "PUT"}}
|
|
for _, role := range []string{"implementation_operator", "site_admin", "viewer"} {
|
|
policies := append([][2]string{}, readPolicies...)
|
|
if role != "viewer" {
|
|
policies = append(policies, writePolicies...)
|
|
}
|
|
for _, policy := range policies {
|
|
rule := deviceCasbinRule{Ptype: "p", V0: role, V1: policy[0], V2: policy[1]}
|
|
if err = tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&rule).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return tx.Create(&common.Migration{Version: version}).Error
|
|
})
|
|
}
|
|
|
|
var supportedLegacyCapabilities = map[string]struct{}{
|
|
"video": {}, "radar": {}, "contact": {}, "button": {}, "wearable": {}, "other": {},
|
|
}
|
|
|
|
type legacyDeviceCapabilitiesRow struct {
|
|
ID string
|
|
Capabilities sql.NullString
|
|
}
|
|
|
|
// prepareLegacyDeviceCapabilities upgrades the pre-GoAdmin text column before
|
|
// GORM sees it. PostgreSQL cannot cast the old empty-string default to jsonb,
|
|
// and old rows stored a single capability token rather than a JSON array.
|
|
func prepareLegacyDeviceCapabilities(tx *gorm.DB) error {
|
|
if tx.Dialector.Name() != "postgres" {
|
|
return nil
|
|
}
|
|
type columnMetadata struct {
|
|
DataType string
|
|
}
|
|
var column columnMetadata
|
|
result := tx.Raw(`SELECT data_type
|
|
FROM information_schema.columns
|
|
WHERE table_schema = current_schema()
|
|
AND table_name = 'sense_devices'
|
|
AND column_name = 'capabilities'`).Scan(&column)
|
|
if result.Error != nil {
|
|
return fmt.Errorf("inspect sense_devices.capabilities: %w", result.Error)
|
|
}
|
|
if result.RowsAffected == 0 || column.DataType == "json" || column.DataType == "jsonb" {
|
|
return nil
|
|
}
|
|
if column.DataType != "text" && column.DataType != "character varying" {
|
|
return fmt.Errorf("sense_devices.capabilities has unsupported legacy type %q", column.DataType)
|
|
}
|
|
if err := tx.Exec(`LOCK TABLE "sense_devices" IN ACCESS EXCLUSIVE MODE`).Error; err != nil {
|
|
return fmt.Errorf("lock sense_devices for capabilities migration: %w", err)
|
|
}
|
|
var rows []legacyDeviceCapabilitiesRow
|
|
if err := tx.Raw(`SELECT id, capabilities FROM "sense_devices" ORDER BY id`).Scan(&rows).Error; err != nil {
|
|
return fmt.Errorf("read legacy device capabilities: %w", err)
|
|
}
|
|
canonical := make(map[string]string, len(rows))
|
|
invalid := 0
|
|
for _, row := range rows {
|
|
value, err := canonicalLegacyCapabilities(row.Capabilities)
|
|
if err != nil {
|
|
invalid++
|
|
continue
|
|
}
|
|
canonical[row.ID] = value
|
|
}
|
|
if invalid > 0 {
|
|
return fmt.Errorf("sense_devices.capabilities contains unsupported legacy data in %d row(s); migration rolled back", invalid)
|
|
}
|
|
if err := tx.Exec(`ALTER TABLE "sense_devices" ALTER COLUMN "capabilities" DROP DEFAULT`).Error; err != nil {
|
|
return fmt.Errorf("drop legacy capabilities default: %w", err)
|
|
}
|
|
for _, row := range rows {
|
|
if err := tx.Exec(`UPDATE "sense_devices" SET "capabilities" = ? WHERE "id" = ?`, canonical[row.ID], row.ID).Error; err != nil {
|
|
return fmt.Errorf("normalize legacy device capabilities: %w", err)
|
|
}
|
|
}
|
|
if err := tx.Exec(`ALTER TABLE "sense_devices" ALTER COLUMN "capabilities" TYPE jsonb USING "capabilities"::jsonb`).Error; err != nil {
|
|
return fmt.Errorf("convert capabilities to jsonb: %w", err)
|
|
}
|
|
if err := tx.Exec(`ALTER TABLE "sense_devices" ALTER COLUMN "capabilities" SET DEFAULT '[]'::jsonb`).Error; err != nil {
|
|
return fmt.Errorf("set jsonb capabilities default: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func canonicalLegacyCapabilities(value sql.NullString) (string, error) {
|
|
if !value.Valid || strings.TrimSpace(value.String) == "" {
|
|
return "[]", nil
|
|
}
|
|
trimmed := strings.TrimSpace(value.String)
|
|
if _, ok := supportedLegacyCapabilities[trimmed]; ok {
|
|
encoded, _ := json.Marshal([]string{trimmed})
|
|
return string(encoded), nil
|
|
}
|
|
if !strings.HasPrefix(trimmed, "[") {
|
|
return "", fmt.Errorf("legacy capability value is not an array")
|
|
}
|
|
var values []string
|
|
if err := json.Unmarshal([]byte(trimmed), &values); err != nil || values == nil || len(values) > 16 {
|
|
return "", fmt.Errorf("legacy capability array is invalid")
|
|
}
|
|
for index, item := range values {
|
|
item = strings.TrimSpace(item)
|
|
if _, ok := supportedLegacyCapabilities[item]; !ok {
|
|
return "", fmt.Errorf("legacy capability array contains an unsupported value")
|
|
}
|
|
values[index] = item
|
|
}
|
|
encoded, _ := json.Marshal(values)
|
|
return string(encoded), nil
|
|
}
|
|
|
|
func ensureDeviceMenu(tx *gorm.DB, desired migrationModels.SysMenu) (migrationModels.SysMenu, error) {
|
|
var menu migrationModels.SysMenu
|
|
err := tx.Where("menu_name = ?", desired.MenuName).First(&menu).Error
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
|
return menu, err
|
|
}
|
|
if err == gorm.ErrRecordNotFound {
|
|
menu = desired
|
|
if err = tx.Create(&menu).Error; err != nil {
|
|
return menu, err
|
|
}
|
|
}
|
|
paths := desired.Paths
|
|
if desired.ParentId == 0 {
|
|
paths = fmt.Sprintf("/0/%d", menu.MenuId)
|
|
}
|
|
err = tx.Model(&menu).Updates(map[string]any{
|
|
"title": desired.Title, "icon": desired.Icon, "path": desired.Path, "paths": paths,
|
|
"menu_type": desired.MenuType, "action": desired.Action, "permission": desired.Permission,
|
|
"parent_id": desired.ParentId, "component": desired.Component, "sort": desired.Sort,
|
|
"visible": desired.Visible, "is_frame": desired.IsFrame,
|
|
}).Error
|
|
return menu, err
|
|
}
|
|
|
|
func attachDeviceRole(tx *gorm.DB, roleKey string, menus []migrationModels.SysMenu) error {
|
|
var role migrationModels.SysRole
|
|
if err := tx.Where("role_key = ?", roleKey).First(&role).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Model(&role).Association("SysMenu").Append(menus)
|
|
}
|