feat: associate dataset sources with owners
This commit is contained in:
@@ -43,6 +43,9 @@ func InitDataset(database *gorm.DB) error {
|
||||
}
|
||||
|
||||
return database.Transaction(func(tx *gorm.DB) error {
|
||||
if err := prepareDatasetSourceOwners(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := prepareDatasetItems(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -55,6 +58,29 @@ func InitDataset(database *gorm.DB) error {
|
||||
})
|
||||
}
|
||||
|
||||
func prepareDatasetSourceOwners(tx *gorm.DB) error {
|
||||
var sources []models.SaDatasetSource
|
||||
if err := tx.Where("owner_id = ? OR owner_identity = ?", 0, "").Find(&sources).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, source := range sources {
|
||||
ownerID := source.OwnerID
|
||||
if ownerID == 0 {
|
||||
ownerID = source.CreatedBy
|
||||
}
|
||||
var owner models.SaUser
|
||||
if err := tx.Select("identity").First(&owner, ownerID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&source).Updates(map[string]any{
|
||||
"owner_id": ownerID, "owner_identity": owner.Identity,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareDatasetItems(tx *gorm.DB) error {
|
||||
if tx.Migrator().HasIndex(&models.SaDatasetItem{}, "idx_sa_dataset_item_source_external") {
|
||||
return nil
|
||||
@@ -85,7 +111,7 @@ func prepareDatasetItems(tx *gorm.DB) error {
|
||||
|
||||
func upsertDefaultDatasetSource(tx *gorm.DB, root models.SaUser, definition defaultDatasetSource) error {
|
||||
var source models.SaDatasetSource
|
||||
err := tx.Where("created_by = ? AND seed_key = ?", root.ID, definition.Key).First(&source).Error
|
||||
err := tx.Where("owner_id = ? AND seed_key = ?", root.ID, definition.Key).First(&source).Error
|
||||
if err == nil {
|
||||
return tx.Model(&source).Updates(map[string]any{
|
||||
"url": definition.URL, "icon_url": definition.IconURL, "kind": "rss",
|
||||
@@ -96,7 +122,7 @@ func upsertDefaultDatasetSource(tx *gorm.DB, root models.SaUser, definition defa
|
||||
}
|
||||
|
||||
knownURLs := append([]string{definition.URL}, definition.LegacyURLs...)
|
||||
err = tx.Where("created_by = ? AND url IN ?", root.ID, knownURLs).First(&source).Error
|
||||
err = tx.Where("owner_id = ? AND url IN ?", root.ID, knownURLs).First(&source).Error
|
||||
if err == nil {
|
||||
return tx.Model(&source).Updates(map[string]any{
|
||||
"seed_key": definition.Key, "url": definition.URL,
|
||||
@@ -109,7 +135,7 @@ func upsertDefaultDatasetSource(tx *gorm.DB, root models.SaUser, definition defa
|
||||
|
||||
seedKey := definition.Key
|
||||
return tx.Create(&models.SaDatasetSource{
|
||||
CreatedBy: root.ID, SeedKey: &seedKey, Name: definition.Name,
|
||||
OwnerID: root.ID, CreatedBy: root.ID, SeedKey: &seedKey, Name: definition.Name,
|
||||
Kind: "rss", URL: definition.URL, IconURL: definition.IconURL,
|
||||
Description: "默认资讯订阅", Enabled: true,
|
||||
}).Error
|
||||
|
||||
@@ -31,6 +31,8 @@ func TestInitDatasetCreatesDefaultSourcesWhenTableIsEmpty(t *testing.T) {
|
||||
require.Equal(t, expected.Key, *sources[index].SeedKey)
|
||||
require.Equal(t, "rss", sources[index].Kind)
|
||||
require.True(t, sources[index].Enabled)
|
||||
require.Equal(t, root.ID, sources[index].OwnerID)
|
||||
require.Equal(t, root.Identity, sources[index].OwnerIdentity)
|
||||
require.Equal(t, root.ID, sources[index].CreatedBy)
|
||||
require.Equal(t, root.Identity, sources[index].CreatedByIdentity)
|
||||
}
|
||||
@@ -60,6 +62,30 @@ func TestInitDatasetAddsMissingDefaultsWithoutChangingCustomSources(t *testing.T
|
||||
require.Nil(t, preserved.SeedKey)
|
||||
}
|
||||
|
||||
func TestInitDatasetBackfillsLegacySourceOwners(t *testing.T) {
|
||||
database := newDatasetInitTestDatabase(t)
|
||||
require.NoError(t, InitUser(database))
|
||||
other := models.SaUser{
|
||||
Email: "other@example.com", DisplayName: "Other",
|
||||
PasswordHash: "hash", Role: "user",
|
||||
}
|
||||
require.NoError(t, database.Create(&other).Error)
|
||||
source := models.SaDatasetSource{
|
||||
CreatedBy: other.ID, Name: "Existing source", Kind: "manual", Enabled: true,
|
||||
}
|
||||
require.NoError(t, database.Create(&source).Error)
|
||||
require.NoError(t, database.Model(&source).Updates(map[string]any{
|
||||
"owner_id": 0, "owner_identity": "",
|
||||
}).Error)
|
||||
|
||||
require.NoError(t, InitDataset(database))
|
||||
|
||||
require.NoError(t, database.First(&source, source.ID).Error)
|
||||
require.Equal(t, other.ID, source.OwnerID)
|
||||
require.Equal(t, other.Identity, source.OwnerIdentity)
|
||||
require.Equal(t, other.ID, source.CreatedBy)
|
||||
}
|
||||
|
||||
func TestInitDatasetRejectsMissingRootInsteadOfAssigningDefaultsToAnotherUser(t *testing.T) {
|
||||
database := newDatasetInitTestDatabase(t)
|
||||
user := models.SaUser{
|
||||
|
||||
@@ -40,6 +40,8 @@ func TestDatasetAPIConnectsSourcesItemsAndSyncTasks(t *testing.T) {
|
||||
require.Empty(t, source.IconURL)
|
||||
var storedSource models.SaDatasetSource
|
||||
require.NoError(t, database.Where("identity = ?", source.ID).First(&storedSource).Error)
|
||||
require.Equal(t, owner.ID, storedSource.OwnerID)
|
||||
require.Equal(t, owner.Identity, storedSource.OwnerIdentity)
|
||||
require.Equal(t, owner.ID, storedSource.CreatedBy)
|
||||
require.Equal(t, owner.Identity, storedSource.CreatedByIdentity)
|
||||
|
||||
@@ -102,6 +104,25 @@ func TestDatasetAPIConnectsSourcesItemsAndSyncTasks(t *testing.T) {
|
||||
require.Zero(t, cronCount)
|
||||
}
|
||||
|
||||
func TestDatasetSourceAuthorizationUsesOwner(t *testing.T) {
|
||||
database := newDatasetTestDatabase(t)
|
||||
owner := createDatasetTestUser(t, database, "owner@example.com")
|
||||
creator := createDatasetTestUser(t, database, "creator@example.com")
|
||||
source := models.SaDatasetSource{
|
||||
OwnerID: owner.ID, CreatedBy: creator.ID,
|
||||
Name: "Owned feed", Kind: "rss", URL: "https://example.com/feed", Enabled: true,
|
||||
}
|
||||
require.NoError(t, database.Create(&source).Error)
|
||||
|
||||
ownerSources, err := NewService(database).ListSources(owner.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ownerSources, 1)
|
||||
|
||||
creatorSources, err := NewService(database).ListSources(creator.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, creatorSources)
|
||||
}
|
||||
|
||||
func newDatasetTestDatabase(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
|
||||
@@ -66,7 +66,7 @@ func newServiceWithFetcher(database *gorm.DB, fetcher FeedFetcher) *Service {
|
||||
|
||||
func (s *Service) ListSources(userID uint) ([]SourceRecord, error) {
|
||||
var sources []models.SaDatasetSource
|
||||
if err := s.db.Where("created_by = ?", userID).Order("created_at asc, id asc").Find(&sources).Error; err != nil {
|
||||
if err := s.db.Where("owner_id = ?", userID).Order("created_at asc, id asc").Find(&sources).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts := make(map[uint]int64)
|
||||
@@ -103,7 +103,7 @@ func (s *Service) CreateSource(userID uint, input SourceInput) (*models.SaDatase
|
||||
return nil, err
|
||||
}
|
||||
source := &models.SaDatasetSource{
|
||||
CreatedBy: userID, Name: normalized.Name, Kind: normalized.Kind,
|
||||
OwnerID: userID, CreatedBy: userID, Name: normalized.Name, Kind: normalized.Kind,
|
||||
URL: normalized.URL, IconURL: normalized.IconURL,
|
||||
Description: normalized.Description, Enabled: normalized.Enabled,
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func (s *Service) UpdateSource(userID uint, identity string, input SourceInput)
|
||||
return nil, err
|
||||
}
|
||||
var source models.SaDatasetSource
|
||||
if err := s.db.Where("identity = ? AND created_by = ?", identity, userID).First(&source).Error; err != nil {
|
||||
if err := s.db.Where("identity = ? AND owner_id = ?", identity, userID).First(&source).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.Model(&source).Updates(map[string]any{
|
||||
@@ -131,7 +131,7 @@ func (s *Service) UpdateSource(userID uint, identity string, input SourceInput)
|
||||
func (s *Service) DeleteSource(userID uint, identity string) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var source models.SaDatasetSource
|
||||
if err := tx.Where("identity = ? AND created_by = ?", identity, userID).First(&source).Error; err != nil {
|
||||
if err := tx.Where("identity = ? AND owner_id = ?", identity, userID).First(&source).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("source_id = ? AND created_by = ?", source.ID, userID).Delete(&models.SaDatasetCron{}).Error; err != nil {
|
||||
@@ -228,10 +228,10 @@ func (s *Service) SyncSources(ctx context.Context, userID uint) ([]models.SaData
|
||||
func (s *Service) SyncAllSources(ctx context.Context) ([]models.SaDatasetCron, error) {
|
||||
var userIDs []uint
|
||||
if err := s.db.Model(&models.SaDatasetSource{}).
|
||||
Distinct("created_by").
|
||||
Distinct("owner_id").
|
||||
Where("enabled = ? AND kind = ?", true, "rss").
|
||||
Order("created_by asc").
|
||||
Pluck("created_by", &userIDs).Error; err != nil {
|
||||
Order("owner_id asc").
|
||||
Pluck("owner_id", &userIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ func (s *Service) SyncAllSources(ctx context.Context) ([]models.SaDatasetCron, e
|
||||
func (s *Service) syncSources(ctx context.Context, userID uint) ([]models.SaDatasetCron, error) {
|
||||
result := make([]models.SaDatasetCron, 0)
|
||||
var sources []models.SaDatasetSource
|
||||
if err := s.db.Where("created_by = ? AND enabled = ? AND kind = ?", userID, true, "rss").
|
||||
if err := s.db.Where("owner_id = ? AND enabled = ? AND kind = ?", userID, true, "rss").
|
||||
Order("id asc").Find(&sources).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -361,7 +361,7 @@ func truncateRunes(value string, limit int) string {
|
||||
|
||||
func (s *Service) findOwnedSource(userID uint, identity string) (*models.SaDatasetSource, error) {
|
||||
var source models.SaDatasetSource
|
||||
err := s.db.Where("identity = ? AND created_by = ?", strings.TrimSpace(identity), userID).First(&source).Error
|
||||
err := s.db.Where("identity = ? AND owner_id = ?", strings.TrimSpace(identity), userID).First(&source).Error
|
||||
return &source, err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import "time"
|
||||
type SaDatasetSource struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
CreatedBy uint `gorm:"index;not null;uniqueIndex:idx_sa_dataset_source_seed,priority:1"`
|
||||
OwnerID uint `gorm:"index;not null;default:0;uniqueIndex:idx_sa_dataset_source_owner_seed,priority:1"`
|
||||
OwnerIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
SeedKey *string `gorm:"size:64;uniqueIndex:idx_sa_dataset_source_seed,priority:2"`
|
||||
SeedKey *string `gorm:"size:64;uniqueIndex:idx_sa_dataset_source_owner_seed,priority:2"`
|
||||
Name string `gorm:"size:160;not null"`
|
||||
Kind string `gorm:"size:32;not null"`
|
||||
URL string `gorm:"size:2048"`
|
||||
|
||||
@@ -22,6 +22,15 @@ func (m *SaDatasetSource) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.OwnerID == 0 {
|
||||
m.OwnerID = m.CreatedBy
|
||||
}
|
||||
if m.CreatedBy == 0 {
|
||||
m.CreatedBy = m.OwnerID
|
||||
}
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.OwnerID, &m.OwnerIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,20 @@ func TestBeforeCreateCopiesParentIdentitiesFromNumericIDs(t *testing.T) {
|
||||
require.Equal(t, item.Identity, *task.SourceInboxItemIdentity)
|
||||
}
|
||||
|
||||
func TestDatasetSourceBeforeCreateAssociatesOwner(t *testing.T) {
|
||||
database := newIdentityTestDB(t)
|
||||
user := SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
source := SaDatasetSource{OwnerID: user.ID, Name: "Owned feed", Kind: "rss"}
|
||||
|
||||
require.NoError(t, database.Create(&source).Error)
|
||||
|
||||
require.Equal(t, user.ID, source.OwnerID)
|
||||
require.Equal(t, user.Identity, source.OwnerIdentity)
|
||||
require.Equal(t, user.ID, source.CreatedBy)
|
||||
require.Equal(t, user.Identity, source.CreatedByIdentity)
|
||||
}
|
||||
|
||||
func newIdentityTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
|
||||
Reference in New Issue
Block a user