diff --git a/internal/api/import.go b/internal/api/import.go index e44a1e8fc..a6169f7b6 100644 --- a/internal/api/import.go +++ b/internal/api/import.go @@ -81,7 +81,7 @@ func StartImport(router *gin.RouterGroup) { RemoveFromFolderCache(entity.RootImport) var destFolder string - if destFolder = s.User().UploadPath; destFolder == "" { + if destFolder = s.User().GetUploadPath(); destFolder == "" { destFolder = conf.ImportDest() } diff --git a/internal/api/users_upload.go b/internal/api/users_upload.go index 23cdbf62b..595e98aec 100644 --- a/internal/api/users_upload.go +++ b/internal/api/users_upload.go @@ -175,7 +175,7 @@ func ProcessUserUpload(router *gin.RouterGroup) { imp := get.Import() var destFolder string - if destFolder = s.User().UploadPath; destFolder == "" { + if destFolder = s.User().GetUploadPath(); destFolder == "" { destFolder = conf.ImportDest() } diff --git a/internal/entity/auth_user.go b/internal/entity/auth_user.go index 9a3723e8e..7ed1790af 100644 --- a/internal/entity/auth_user.go +++ b/internal/entity/auth_user.go @@ -15,6 +15,7 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/list" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt" ) @@ -352,19 +353,49 @@ func (m *User) CanUpload() bool { } } -// SetBasePath changes the user's base folder. +// GetBasePath returns the user's relative base path. +func (m *User) GetBasePath() string { + return m.BasePath +} + +// SetBasePath changes the user's relative base path. func (m *User) SetBasePath(dir string) *User { - m.BasePath = clean.UserPath(dir) + if list.Contains(list.List{"", ".", "./", "/", "\\"}, dir) { + m.BasePath = "" + } else if dir == "~" && m.UserUID != "" { + m.BasePath = fmt.Sprintf("users/%s", m.UserUID) + } else { + m.BasePath = clean.UserPath(dir) + } return m } -// SetUploadPath changes the user's upload folder. +// GetUploadPath returns the user's relative upload path. +func (m *User) GetUploadPath() string { + basePath := m.GetBasePath() + + if list.Contains(list.List{"", ".", "./"}, m.UploadPath) { + return basePath + } else if basePath != "" && strings.HasPrefix(m.UploadPath, basePath+"/") { + return m.UploadPath + } else if basePath == "" && m.UploadPath == "~" && m.UserUID != "" { + return fmt.Sprintf("users/%s", m.UserUID) + } + + return path.Join(basePath, m.UploadPath) +} + +// SetUploadPath changes the user's relative upload path. func (m *User) SetUploadPath(dir string) *User { - if m.BasePath == "" { - m.UploadPath = clean.UserPath(dir) + basePath := m.GetBasePath() + + if list.Contains(list.List{"", ".", "./", "/", "\\"}, dir) { + m.UploadPath = "" + } else if basePath == "" && dir == "~" && m.UserUID != "" { + m.UploadPath = fmt.Sprintf("users/%s", m.UserUID) } else { - m.UploadPath = path.Join(m.BasePath, clean.UserPath(dir)) + m.UploadPath = clean.UserPath(dir) } return m @@ -432,13 +463,19 @@ func (m *User) Email() string { return clean.Email(m.UserEmail) } +// Handle returns the user's login handle. +func (m *User) Handle() string { + handle, _, _ := strings.Cut(m.UserName, "@") + return handle +} + // FullName returns the name of the user for display purposes. func (m *User) FullName() string { switch { case m.DisplayName != "": return m.DisplayName default: - return m.UserName + return clean.NameCapitalized(strings.ReplaceAll(m.Handle(), ".", " ")) } } @@ -837,19 +874,14 @@ func (m *User) SaveForm(f form.User) error { func (m *User) SetDisplayName(name string) *User { name = clean.Name(name) - // Empty? - if name == "" { + d := m.Details() + + if name == "" || SrcPriority[SrcAuto] < SrcPriority[d.NameSrc] { return m } m.DisplayName = name - d := m.Details() - - if SrcPriority[SrcAuto] < SrcPriority[d.NameSrc] { - return m - } - // Try to parse name into components. n := txt.ParseName(name) @@ -863,6 +895,18 @@ func (m *User) SetDisplayName(name string) *User { return m } +// SetGivenName updates the user's given name. +func (m *User) SetGivenName(name string) *User { + m.Details().SetGivenName(name) + return m +} + +// SetFamilyName updates the user's family name. +func (m *User) SetFamilyName(name string) *User { + m.Details().SetFamilyName(name) + return m +} + // SetAvatar updates the user avatar image. func (m *User) SetAvatar(thumb, thumbSrc string) error { if m.UserName == "" || m.ID <= 0 { diff --git a/internal/entity/auth_user_details.go b/internal/entity/auth_user_details.go index 1cbeff1b6..1b0d6b701 100644 --- a/internal/entity/auth_user_details.go +++ b/internal/entity/auth_user_details.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -100,3 +101,29 @@ func (m *UserDetails) Save() error { func (m *UserDetails) Updates(values interface{}) error { return UnscopedDb().Model(m).Updates(values).Error } + +// SetGivenName updates the user's given name. +func (m *UserDetails) SetGivenName(name string) *UserDetails { + name = clean.Name(name) + + if name == "" || SrcPriority[SrcAuto] < SrcPriority[m.NameSrc] { + return m + } + + m.GivenName = name + + return m +} + +// SetFamilyName updates the user's family name. +func (m *UserDetails) SetFamilyName(name string) *UserDetails { + name = clean.Name(name) + + if name == "" || SrcPriority[SrcAuto] < SrcPriority[m.NameSrc] { + return m + } + + m.FamilyName = name + + return m +} diff --git a/internal/entity/auth_user_test.go b/internal/entity/auth_user_test.go index 9d9d217e7..d37767272 100644 --- a/internal/entity/auth_user_test.go +++ b/internal/entity/auth_user_test.go @@ -838,3 +838,129 @@ func TestUser_Provider(t *testing.T) { assert.Equal(t, "password", Admin.Provider()) }) } + +func TestUser_GetBasePath(t *testing.T) { + t.Run("Visitor", func(t *testing.T) { + assert.Equal(t, "", Visitor.GetBasePath()) + }) + t.Run("UnknownUser", func(t *testing.T) { + assert.Equal(t, "", UnknownUser.GetBasePath()) + }) + t.Run("Admin", func(t *testing.T) { + assert.Equal(t, "", Admin.GetBasePath()) + }) +} + +func TestUser_SetBasePath(t *testing.T) { + t.Run("Test", func(t *testing.T) { + u := User{ + ID: 1234567, + UserUID: "urqdrfb72479n047", + UserName: "test", + UserRole: acl.RoleAdmin.String(), + DisplayName: "Test", + SuperAdmin: false, + CanLogin: true, + WebDAV: true, + CanInvite: false, + } + + assert.Equal(t, "base", u.SetBasePath("base").GetBasePath()) + assert.Equal(t, "users/urqdrfb72479n047", u.SetBasePath("~").GetBasePath()) + assert.Equal(t, "users/urqdrfb72479n047", u.GetUploadPath()) + }) +} + +func TestUser_GetUploadPath(t *testing.T) { + t.Run("Visitor", func(t *testing.T) { + assert.Equal(t, "", Visitor.GetUploadPath()) + }) + t.Run("UnknownUser", func(t *testing.T) { + assert.Equal(t, "", UnknownUser.GetUploadPath()) + }) + t.Run("Admin", func(t *testing.T) { + assert.Equal(t, "", Admin.GetUploadPath()) + }) +} + +func TestUser_SetUploadPath(t *testing.T) { + t.Run("Test", func(t *testing.T) { + u := User{ + ID: 1234567, + UserUID: "urqdrfb72479n047", + UserName: "test", + UserRole: acl.RoleAdmin.String(), + DisplayName: "Test", + SuperAdmin: false, + CanLogin: true, + WebDAV: true, + CanInvite: false, + } + + assert.Equal(t, "upload", u.SetUploadPath("upload").GetUploadPath()) + assert.Equal(t, "base/upload", u.SetBasePath("base").GetUploadPath()) + assert.Equal(t, "base", u.SetUploadPath("~").GetUploadPath()) + assert.Equal(t, "users/urqdrfb72479n047", u.SetBasePath("~").GetUploadPath()) + }) +} + +func TestUser_Handle(t *testing.T) { + t.Run("Default", func(t *testing.T) { + u := User{ + ID: 1234567, + UserUID: "urqdrfb72479n047", + UserName: "mr-happy@cat.com", + UserRole: acl.RoleAdmin.String(), + DisplayName: "", + SuperAdmin: false, + CanLogin: true, + WebDAV: true, + CanInvite: false, + } + + assert.Equal(t, "mr-happy@cat.com", u.Login()) + assert.Equal(t, "mr-happy", u.Handle()) + + u.UserName = "mr.happy@cat.com" + + assert.Equal(t, "mr.happy", u.Handle()) + + u.UserName = "mr.happy" + + assert.Equal(t, "mr.happy", u.Handle()) + }) +} + +func TestUser_FullName(t *testing.T) { + t.Run("Default", func(t *testing.T) { + u := User{ + ID: 1234567, + UserUID: "urqdrfb72479n047", + UserName: "mr-happy@cat.com", + UserRole: acl.RoleAdmin.String(), + DisplayName: "", + SuperAdmin: false, + CanLogin: true, + WebDAV: true, + CanInvite: false, + } + + assert.Equal(t, "Mr-Happy", u.FullName()) + + u.UserName = "mr.happy@cat.com" + + assert.Equal(t, "Mr Happy", u.FullName()) + + u.UserName = "mr.happy" + + assert.Equal(t, "Mr Happy", u.FullName()) + + u.UserName = "foo@bar.com" + + assert.Equal(t, "Foo", u.FullName()) + + u.SetDisplayName("Jane Doe") + + assert.Equal(t, "Jane Doe", u.FullName()) + }) +} diff --git a/internal/search/albums.go b/internal/search/albums.go index ebf5a709d..6695df160 100644 --- a/internal/search/albums.go +++ b/internal/search/albums.go @@ -65,11 +65,11 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults if sess.IsVisitor() || sess.NotRegistered() { s = s.Where("albums.album_uid IN (?) OR albums.published_at > ?", sess.SharedUIDs(), entity.TimeStamp()) } else if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) { - if user.BasePath == "" { + if basePath := user.GetBasePath(); basePath == "" { s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ?", sess.SharedUIDs(), user.UserUID, entity.TimeStamp()) } else { s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ? OR albums.album_type = ? AND (albums.album_path = ? OR albums.album_path LIKE ?)", - sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), entity.AlbumFolder, user.BasePath, user.BasePath+"/%") + sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), entity.AlbumFolder, basePath, basePath+"/%") } } diff --git a/internal/search/photos.go b/internal/search/photos.go index 22533718c..6590b1f08 100644 --- a/internal/search/photos.go +++ b/internal/search/photos.go @@ -127,11 +127,11 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) if sess.IsVisitor() || sess.NotRegistered() { s = s.Where(sharedAlbums+"photos.published_at > ?", sess.SharedUIDs(), entity.TimeStamp()) - } else if user.BasePath == "" { + } else if basePath := user.GetBasePath(); basePath == "" { s = s.Where(sharedAlbums+"photos.created_by = ? OR photos.published_at > ?", sess.SharedUIDs(), user.UserUID, entity.TimeStamp()) } else { s = s.Where(sharedAlbums+"photos.created_by = ? OR photos.published_at > ? OR photos.photo_path = ? OR photos.photo_path LIKE ?", - sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), user.BasePath, user.BasePath+"/%") + sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), basePath, basePath+"/%") } } } diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go index c15e0c6a9..a91a11c52 100644 --- a/internal/search/photos_geo.go +++ b/internal/search/photos_geo.go @@ -125,11 +125,11 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes if sess.IsVisitor() || sess.NotRegistered() { s = s.Where(sharedAlbums+"photos.published_at > ?", sess.SharedUIDs(), entity.TimeStamp()) - } else if user.BasePath == "" { + } else if basePath := user.GetBasePath(); basePath == "" { s = s.Where(sharedAlbums+"photos.created_by = ? OR photos.published_at > ?", sess.SharedUIDs(), user.UserUID, entity.TimeStamp()) } else { s = s.Where(sharedAlbums+"photos.created_by = ? OR photos.published_at > ? OR photos.photo_path = ? OR photos.photo_path LIKE ?", - sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), user.BasePath, user.BasePath+"/%") + sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), basePath, basePath+"/%") } } }