feat: harden loose-offline sync for user JWT, schema, and console ops
Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Binding 本机库 ↔ 线上库映射(P1;不挡推送,供登记/查询)。
|
||||
// Binding 本机库 ↔ 线上库映射(登记/查询;用户自助 push 时按 user_id+online_db_id 鉴权)。
|
||||
type Binding struct {
|
||||
ID string `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
@@ -19,9 +19,12 @@ type Binding struct {
|
||||
LocalDatabaseID string `json:"local_database_id"`
|
||||
OnlineDBID string `json:"online_db_id"`
|
||||
ChannelID string `json:"channel_id,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// 可读名(与宇恒 database_name / display_name 对齐;控制台优先展示)
|
||||
DatabaseName string `json:"database_name,omitempty"` // 本地库可读名
|
||||
DisplayName string `json:"display_name,omitempty"` // 线上库可读名
|
||||
Note string `json:"note,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s *FileStore) bindingPath() string {
|
||||
@@ -37,6 +40,8 @@ func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
|
||||
}
|
||||
b.LocalDatabaseID = strings.TrimSpace(b.LocalDatabaseID)
|
||||
b.OnlineDBID = strings.TrimSpace(b.OnlineDBID)
|
||||
b.DatabaseName = strings.TrimSpace(b.DatabaseName)
|
||||
b.DisplayName = strings.TrimSpace(b.DisplayName)
|
||||
if b.TenantID <= 0 {
|
||||
return b, fmt.Errorf("tenant_id required")
|
||||
}
|
||||
@@ -53,6 +58,12 @@ func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
|
||||
if b.ChannelID != "" {
|
||||
list[i].ChannelID = b.ChannelID
|
||||
}
|
||||
if b.DatabaseName != "" {
|
||||
list[i].DatabaseName = b.DatabaseName
|
||||
}
|
||||
if b.DisplayName != "" {
|
||||
list[i].DisplayName = b.DisplayName
|
||||
}
|
||||
if b.Note != "" {
|
||||
list[i].Note = b.Note
|
||||
}
|
||||
@@ -79,6 +90,11 @@ func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
|
||||
}
|
||||
|
||||
func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Binding, error) {
|
||||
return s.ListBindingsFiltered(tenantID, 0, localDatabaseID)
|
||||
}
|
||||
|
||||
// ListBindingsFiltered 按租户列出;userID>0 时仅返回该用户的 Binding。
|
||||
func (s *FileStore) ListBindingsFiltered(tenantID, userID int64, localDatabaseID string) ([]Binding, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
list, err := s.readBindingsUnlocked()
|
||||
@@ -90,6 +106,9 @@ func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Bind
|
||||
if b.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if userID > 0 && b.UserID != userID {
|
||||
continue
|
||||
}
|
||||
if localDatabaseID != "" && b.LocalDatabaseID != localDatabaseID {
|
||||
continue
|
||||
}
|
||||
@@ -98,6 +117,47 @@ func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Bind
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UserOwnsOnlineDB 用户是否登记了该 online_db_id(可选限定 channel)。
|
||||
func (s *FileStore) UserOwnsOnlineDB(tenantID, userID int64, channelID, onlineDBID string) bool {
|
||||
if tenantID <= 0 || userID <= 0 || strings.TrimSpace(onlineDBID) == "" {
|
||||
return false
|
||||
}
|
||||
list, err := s.ListBindingsFiltered(tenantID, userID, "")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
online := strings.TrimSpace(onlineDBID)
|
||||
ch := strings.TrimSpace(channelID)
|
||||
for _, b := range list {
|
||||
if strings.TrimSpace(b.OnlineDBID) != online {
|
||||
continue
|
||||
}
|
||||
if ch != "" && b.ChannelID != "" && b.ChannelID != ch {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UserCanAccessChannel 用户是否登记了指向该通道的 Binding(channel_id 空视为未限定通道)。
|
||||
func (s *FileStore) UserCanAccessChannel(tenantID, userID int64, channelID string) bool {
|
||||
if tenantID <= 0 || userID <= 0 || strings.TrimSpace(channelID) == "" {
|
||||
return false
|
||||
}
|
||||
list, err := s.ListBindingsFiltered(tenantID, userID, "")
|
||||
if err != nil || len(list) == 0 {
|
||||
return false
|
||||
}
|
||||
ch := strings.TrimSpace(channelID)
|
||||
for _, b := range list {
|
||||
if b.ChannelID == "" || b.ChannelID == ch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *FileStore) GetBinding(tenantID int64, localDatabaseID string) (*Binding, error) {
|
||||
list, err := s.ListBindings(tenantID, localDatabaseID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user