From 693108fd53a10f019b4230734cd8b8d9336e6562 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 4 Oct 2022 00:54:39 +0200 Subject: [PATCH] Sharing: Refactor UserShare entity #98 #782 Signed-off-by: Michael Mayer --- internal/entity/auth_user.go | 16 +++---- .../{auth_share.go => auth_user_share.go} | 44 +++++++++---------- ...ixtures.go => auth_user_share_fixtures.go} | 20 ++++----- ..._share_test.go => auth_user_share_test.go} | 16 +++---- internal/entity/entity_tables.go | 2 +- internal/entity/fixtures.go | 2 +- internal/entity/link.go | 25 +++++++++-- 7 files changed, 71 insertions(+), 54 deletions(-) rename internal/entity/{auth_share.go => auth_user_share.go} (77%) rename internal/entity/{auth_share_fixtures.go => auth_user_share_fixtures.go} (58%) rename internal/entity/{auth_share_test.go => auth_user_share_test.go} (80%) diff --git a/internal/entity/auth_user.go b/internal/entity/auth_user.go index a3686ca2e..e97809d36 100644 --- a/internal/entity/auth_user.go +++ b/internal/entity/auth_user.go @@ -62,6 +62,7 @@ type User struct { BornAt *time.Time `sql:"index" json:"BornAt,omitempty" yaml:"BornAt,omitempty"` UserDetails *UserDetails `gorm:"PRELOAD:true;foreignkey:UserUID;association_foreignkey:UserUID;" json:"Details,omitempty" yaml:"Details,omitempty"` UserSettings *UserSettings `gorm:"PRELOAD:true;foreignkey:UserUID;association_foreignkey:UserUID;" json:"Settings,omitempty" yaml:"Settings,omitempty"` + UserShares UserShares `gorm:"-" json:"Shares,omitempty" yaml:"Shares,omitempty"` ResetToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"` PreviewToken string `gorm:"type:VARBINARY(64);column:preview_token;" json:"-" yaml:"-"` DownloadToken string `gorm:"type:VARBINARY(64);column:download_token;" json:"-" yaml:"-"` @@ -71,7 +72,6 @@ type User struct { CreatedAt time.Time `json:"CreatedAt" yaml:"-"` UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"` DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"` - Shares Shares `gorm:"-" json:"Shares,omitempty" yaml:"Shares,omitempty"` } // TableName returns the entity table name. @@ -635,7 +635,7 @@ func (m *User) SetFormValues(frm form.User) *User { // RefreshShares updates the list of shares. func (m *User) RefreshShares() *User { - m.Shares = FindShares(m.UID()) + m.UserShares = FindUserShares(m.UID()) return m } @@ -645,7 +645,7 @@ func (m *User) NoShares() bool { return true } - return m.Shares.Empty() + return m.UserShares.Empty() } // HasShares checks if the user has any shares. @@ -659,16 +659,16 @@ func (m *User) HasShare(uid string) bool { return false } - return m.Shares.Contains(uid) + return m.UserShares.Contains(uid) } // SharedUIDs returns shared entity UIDs. func (m *User) SharedUIDs() UIDs { - if m.IsRegistered() && m.Shares.Empty() { + if m.IsRegistered() && m.UserShares.Empty() { m.RefreshShares() } - return m.Shares.UIDs() + return m.UserShares.UIDs() } // RedeemToken updates shared entity UIDs using the specified token. @@ -687,8 +687,8 @@ func (m *User) RedeemToken(token string) (n int) { // Find shares. for _, link := range links { - if found := FindShare(Share{UserUID: m.UID(), ShareUID: link.ShareUID}); found == nil { - share := NewShare(m.UID(), link.ShareUID, link.Perm, link.ExpiresAt()) + if found := FindUserShare(UserShare{UserUID: m.UID(), ShareUID: link.ShareUID}); found == nil { + share := NewUserShare(m.UID(), link.ShareUID, link.Perm, link.ExpiresAt()) share.LinkUID = link.LinkUID share.Comment = link.Comment diff --git a/internal/entity/auth_share.go b/internal/entity/auth_user_share.go similarity index 77% rename from internal/entity/auth_share.go rename to internal/entity/auth_user_share.go index ef85b558f..ba2249c18 100644 --- a/internal/entity/auth_share.go +++ b/internal/entity/auth_user_share.go @@ -27,11 +27,11 @@ const ( SharePrefix = "share" ) -// Shares represents shared content. -type Shares []Share +// UserShares represents shared content. +type UserShares []UserShare // UIDs returns shared UIDs. -func (m Shares) UIDs() UIDs { +func (m UserShares) UIDs() UIDs { result := make(UIDs, len(m)) for i, share := range m { @@ -42,12 +42,12 @@ func (m Shares) UIDs() UIDs { } // Empty checks if there are no shares. -func (m Shares) Empty() bool { +func (m UserShares) Empty() bool { return m == nil || len(m) == 0 } // Contains checks the uid is shared. -func (m Shares) Contains(uid string) bool { +func (m UserShares) Contains(uid string) bool { if len(m) == 0 { return false } @@ -61,8 +61,8 @@ func (m Shares) Contains(uid string) bool { return false } -// Share represents content shared with a user. -type Share struct { +// UserShare represents content shared with a user. +type UserShare struct { UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"-" yaml:"UserUID"` ShareUID string `gorm:"type:VARBINARY(42);primary_key;index;" json:"ShareUID" yaml:"ShareUID"` LinkUID string `gorm:"type:VARBINARY(42);" json:"LinkUID,omitempty" yaml:"LinkUID,omitempty"` @@ -75,13 +75,13 @@ type Share struct { } // TableName returns the entity table name. -func (Share) TableName() string { +func (UserShare) TableName() string { return "auth_users_shares" } -// NewShare creates a new entity model. -func NewShare(userUID, shareUid string, perm uint, expires *time.Time) *Share { - result := &Share{ +// NewUserShare creates a new entity model. +func NewUserShare(userUID, shareUid string, perm uint, expires *time.Time) *UserShare { + result := &UserShare{ UserUID: userUID, ShareUID: shareUid, Perm: perm, @@ -94,13 +94,13 @@ func NewShare(userUID, shareUid string, perm uint, expires *time.Time) *Share { return result } -// FindShare fetches the matching record or returns null if it was not found. -func FindShare(find Share) *Share { +// FindUserShare fetches the matching record or returns null if it was not found. +func FindUserShare(find UserShare) *UserShare { if !find.HasID() { return nil } - m := &Share{} + m := &UserShare{} // Find matching record. if UnscopedDb().First(m, "user_uid = ? AND share_uid = ?", find.UserUID, find.ShareUID).RecordNotFound() { @@ -110,9 +110,9 @@ func FindShare(find Share) *Share { return m } -// FindShares finds all shares to which the user has access. -func FindShares(userUid string) Shares { - found := Shares{} +// FindUserShares finds all shares to which the user has access. +func FindUserShares(userUid string) UserShares { + found := UserShares{} if rnd.InvalidUID(userUid, UserUID) { return found @@ -128,27 +128,27 @@ func FindShares(userUid string) Shares { } // HasID tests if the entity has a valid uid. -func (m *Share) HasID() bool { +func (m *UserShare) HasID() bool { return rnd.IsUID(m.UserUID, UserUID) && rnd.IsUID(m.ShareUID, 0) } // Create inserts a new record into the database. -func (m *Share) Create() error { +func (m *UserShare) Create() error { return Db().Create(m).Error } // Save updates the record in the database or inserts a new record if it does not already exist. -func (m *Share) Save() error { +func (m *UserShare) Save() error { return Db().Save(m).Error } // Updates changes multiple record values. -func (m *Share) Updates(values interface{}) error { +func (m *UserShare) Updates(values interface{}) error { return UnscopedDb().Model(m).Updates(values).Error } // UpdateLink updates the share data using the Link provided. -func (m *Share) UpdateLink(link Link) error { +func (m *UserShare) UpdateLink(link Link) error { if m.ShareUID != link.ShareUID { return fmt.Errorf("shared uid does not match") } diff --git a/internal/entity/auth_share_fixtures.go b/internal/entity/auth_user_share_fixtures.go similarity index 58% rename from internal/entity/auth_share_fixtures.go rename to internal/entity/auth_user_share_fixtures.go index efb7365dd..193b30ed0 100644 --- a/internal/entity/auth_share_fixtures.go +++ b/internal/entity/auth_user_share_fixtures.go @@ -4,28 +4,28 @@ import ( "github.com/photoprism/photoprism/pkg/rnd" ) -type ShareMap map[string]Share +type UserShareMap map[string]UserShare // Get returns a fixture for use in tests. -func (m ShareMap) Get(name string) Share { +func (m UserShareMap) Get(name string) UserShare { if result, ok := m[name]; ok { return result } - return Share{} + return UserShare{} } // Pointer returns a fixture pointer for use in tests. -func (m ShareMap) Pointer(name string) *Share { +func (m UserShareMap) Pointer(name string) *UserShare { if result, ok := m[name]; ok { return &result } - return &Share{} + return &UserShare{} } -// ShareFixtures specifies fixtures for use in tests. -var ShareFixtures = ShareMap{ +// UserShareFixtures specifies fixtures for use in tests. +var UserShareFixtures = UserShareMap{ "AliceAlbum": { UserUID: "uqxetse3cy5eo9z2", ShareUID: "at9lxuqxpogaaba9", @@ -38,9 +38,9 @@ var ShareFixtures = ShareMap{ }, } -// CreateShareFixtures creates the fixtures specified above. -func CreateShareFixtures() { - for _, entity := range ShareFixtures { +// CreateUserShareFixtures creates the fixtures specified above. +func CreateUserShareFixtures() { + for _, entity := range UserShareFixtures { Db().Create(&entity) } } diff --git a/internal/entity/auth_share_test.go b/internal/entity/auth_user_share_test.go similarity index 80% rename from internal/entity/auth_share_test.go rename to internal/entity/auth_user_share_test.go index 059ef2b26..fb906fd1b 100644 --- a/internal/entity/auth_share_test.go +++ b/internal/entity/auth_user_share_test.go @@ -9,9 +9,9 @@ import ( "github.com/photoprism/photoprism/pkg/rnd" ) -func TestNewShare(t *testing.T) { +func TestNewUserShare(t *testing.T) { expires := TimeStamp().Add(time.Hour * 48) - m := NewShare(Admin.UID(), AlbumFixtures.Get("berlin-2019").AlbumUID, PermReact, &expires) + m := NewUserShare(Admin.UID(), AlbumFixtures.Get("berlin-2019").AlbumUID, PermReact, &expires) assert.True(t, m.HasID()) assert.True(t, rnd.IsRefID(m.RefID)) @@ -35,11 +35,11 @@ func TestPerm(t *testing.T) { assert.Equal(t, uint(128), PermAll) } -func TestFindShare(t *testing.T) { +func TestFindUserShare(t *testing.T) { t.Run("AliceAlbum", func(t *testing.T) { - m := FindShare(Share{UserUID: "uqxetse3cy5eo9z2", ShareUID: "at9lxuqxpogaaba9"}) + m := FindUserShare(UserShare{UserUID: "uqxetse3cy5eo9z2", ShareUID: "at9lxuqxpogaaba9"}) - expected := ShareFixtures.Get("AliceAlbum") + expected := UserShareFixtures.Get("AliceAlbum") assert.NotNil(t, m) assert.True(t, m.HasID()) @@ -55,13 +55,13 @@ func TestFindShare(t *testing.T) { }) } -func TestFindShares(t *testing.T) { - found := FindShares(UserFixtures.Pointer("alice").UID()) +func TestFindUserShares(t *testing.T) { + found := FindUserShares(UserFixtures.Pointer("alice").UID()) assert.NotNil(t, found) assert.Len(t, found, 1) m := found[0] - expected := ShareFixtures.Get("AliceAlbum") + expected := UserShareFixtures.Get("AliceAlbum") assert.NotNil(t, m) assert.True(t, m.HasID()) diff --git a/internal/entity/entity_tables.go b/internal/entity/entity_tables.go index 58c781592..c03f67786 100644 --- a/internal/entity/entity_tables.go +++ b/internal/entity/entity_tables.go @@ -48,7 +48,7 @@ var Entities = Tables{ Face{}.TableName(): &Face{}, Marker{}.TableName(): &Marker{}, Reaction{}.TableName(): &Reaction{}, - Share{}.TableName(): &Share{}, + UserShare{}.TableName(): &UserShare{}, } // WaitForMigration waits for the database migration to be successful. diff --git a/internal/entity/fixtures.go b/internal/entity/fixtures.go index 1acb8d7f7..2559eb5d0 100644 --- a/internal/entity/fixtures.go +++ b/internal/entity/fixtures.go @@ -31,5 +31,5 @@ func CreateTestFixtures() { CreateSessionFixtures() CreateReactionFixtures() CreatePasswordFixtures() - CreateShareFixtures() + CreateUserShareFixtures() } diff --git a/internal/entity/link.go b/internal/entity/link.go index 38490310a..0dc8a37c5 100644 --- a/internal/entity/link.go +++ b/internal/entity/link.go @@ -4,17 +4,18 @@ import ( "fmt" "time" - "github.com/photoprism/photoprism/internal/event" - "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt" ) +// LinkPrefix for RefID. const ( - LinkUID = byte('s') + LinkUID = byte('s') + LinkPrefix = "link" ) type Links []Link @@ -45,7 +46,7 @@ func (Link) TableName() string { // BeforeCreate creates a random UID if needed before inserting a new row to the database. func (m *Link) BeforeCreate(scope *gorm.Scope) error { if rnd.InvalidRefID(m.RefID) { - m.RefID = rnd.RefID(SessionPrefix) + m.RefID = rnd.RefID(LinkPrefix) Log("link", "set ref id", scope.SetColumn("RefID", m.RefID)) } @@ -165,6 +166,13 @@ func (m *Link) Save() error { func (m *Link) Delete() error { if m.LinkToken == "" { return fmt.Errorf("empty link token") + } else if m.LinkUID == "" { + return fmt.Errorf("empty link uid") + } + + // Remove related user shares. + if err := UnscopedDb().Delete(UserShare{}, "link_uid = ?", m.LinkUID).Error; err != nil { + event.AuditErr([]string{"link %s", "failed to remove related user shares", "%s"}, clean.Log(m.RefID), err) } return Db().Delete(m).Error @@ -172,6 +180,15 @@ func (m *Link) Delete() error { // DeleteShareLinks removes all links that match the shared UID. func DeleteShareLinks(shareUid string) error { + if shareUid == "" { + return fmt.Errorf("empty share uid") + } + + // Remove related user shares. + if err := UnscopedDb().Delete(UserShare{}, "share_uid = ?", shareUid).Error; err != nil { + event.AuditErr([]string{"share %s", "failed to remove related user shares", "%s"}, clean.Log(shareUid), err) + } + return Db().Delete(&Link{}, "share_uid = ?", shareUid).Error }