mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-02 08:27:56 +01:00
feat(database): add user_library table and library access methods
Signed-off-by: Deluan <deluan@navidrome.org> # Conflicts: # tests/mock_library_repo.go
This commit is contained in:
parent
3c1e5603d0
commit
40bbae16c4
9 changed files with 533 additions and 6 deletions
|
|
@ -2,6 +2,7 @@ package persistence
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -87,6 +88,17 @@ func (r *libraryRepository) Put(l *model.Library) error {
|
|||
defer libLock.Unlock()
|
||||
libCache[l.ID] = l.Path
|
||||
}
|
||||
|
||||
// Auto-assign library to all admin users
|
||||
if err == nil {
|
||||
sql := Expr(
|
||||
"INSERT INTO user_library (user_id, library_id) SELECT id, ? FROM user WHERE is_admin = true ON CONFLICT (user_id, library_id) DO NOTHING",
|
||||
l.ID,
|
||||
)
|
||||
if _, addErr := r.executeSQL(sql); addErr != nil {
|
||||
return fmt.Errorf("failed to assign library to admin users: %w", addErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -206,4 +218,18 @@ func (r *libraryRepository) GetAll(ops ...model.QueryOptions) (model.Libraries,
|
|||
return res, err
|
||||
}
|
||||
|
||||
// User-library association methods
|
||||
|
||||
func (r *libraryRepository) GetUsersWithLibraryAccess(libraryID int) (model.Users, error) {
|
||||
sel := Select("u.*").
|
||||
From("user u").
|
||||
Join("user_library ul ON u.id = ul.user_id").
|
||||
Where(Eq{"ul.library_id": libraryID}).
|
||||
OrderBy("u.name")
|
||||
|
||||
var res model.Users
|
||||
err := r.queryAll(sel, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ model.LibraryRepository = (*libraryRepository)(nil)
|
||||
|
|
|
|||
|
|
@ -79,18 +79,34 @@ func (r *userRepository) Put(u *model.User) error {
|
|||
return fmt.Errorf("error converting user to SQL args: %w", err)
|
||||
}
|
||||
delete(values, "current_password")
|
||||
|
||||
// Save/update the user
|
||||
update := Update(r.tableName).Where(Eq{"id": u.ID}).SetMap(values)
|
||||
count, err := r.executeSQL(update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
if count == 0 {
|
||||
values["created_at"] = time.Now()
|
||||
insert := Insert(r.tableName).SetMap(values)
|
||||
_, err = r.executeSQL(insert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
values["created_at"] = time.Now()
|
||||
insert := Insert(r.tableName).SetMap(values)
|
||||
_, err = r.executeSQL(insert)
|
||||
return err
|
||||
|
||||
// Auto-assign all libraries to admin users in a single SQL operation
|
||||
if u.IsAdmin {
|
||||
sql := Expr(
|
||||
"INSERT OR IGNORE INTO user_library (user_id, library_id) SELECT ?, id FROM library",
|
||||
u.ID,
|
||||
)
|
||||
if _, err := r.executeSQL(sql); err != nil {
|
||||
return fmt.Errorf("failed to assign all libraries to admin user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindFirstAdmin() (*model.User, error) {
|
||||
|
|
@ -365,6 +381,57 @@ func (r *userRepository) decryptAllPasswords(users model.Users) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Library association methods
|
||||
|
||||
func (r *userRepository) GetUserLibraries(userID string) (model.Libraries, error) {
|
||||
sel := Select("l.*").
|
||||
From("library l").
|
||||
Join("user_library ul ON l.id = ul.library_id").
|
||||
Where(Eq{"ul.user_id": userID}).
|
||||
OrderBy("l.name")
|
||||
|
||||
var res model.Libraries
|
||||
err := r.queryAll(sel, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *userRepository) SetUserLibraries(userID string, libraryIDs []int) error {
|
||||
// Remove existing associations
|
||||
delSql := Delete("user_library").Where(Eq{"user_id": userID})
|
||||
if _, err := r.executeSQL(delSql); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add new associations
|
||||
if len(libraryIDs) > 0 {
|
||||
insert := Insert("user_library").Columns("user_id", "library_id")
|
||||
for _, libID := range libraryIDs {
|
||||
insert = insert.Values(userID, libID)
|
||||
}
|
||||
_, err := r.executeSQL(insert)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) AddUserLibrary(userID string, libraryID int) error {
|
||||
insert := Insert("user_library").
|
||||
Columns("user_id", "library_id").
|
||||
Values(userID, libraryID).
|
||||
Suffix("ON CONFLICT (user_id, library_id) DO NOTHING")
|
||||
_, err := r.executeSQL(insert)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *userRepository) RemoveUserLibrary(userID string, libraryID int) error {
|
||||
del := Delete("user_library").Where(And{
|
||||
Eq{"user_id": userID},
|
||||
Eq{"library_id": libraryID},
|
||||
})
|
||||
_, err := r.executeSQL(del)
|
||||
return err
|
||||
}
|
||||
|
||||
var _ model.UserRepository = (*userRepository)(nil)
|
||||
var _ rest.Repository = (*userRepository)(nil)
|
||||
var _ rest.Persistable = (*userRepository)(nil)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
|
|
@ -235,4 +236,260 @@ var _ = Describe("UserRepository", func() {
|
|||
Expect(err).To(MatchError("fake error"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Library Association Methods", func() {
|
||||
var userID string
|
||||
var library1, library2 model.Library
|
||||
|
||||
BeforeEach(func() {
|
||||
// Create a test user first to satisfy foreign key constraints
|
||||
testUser := model.User{
|
||||
ID: "test-user-id",
|
||||
UserName: "testuser",
|
||||
Name: "Test User",
|
||||
Email: "test@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
Expect(repo.Put(&testUser)).To(BeNil())
|
||||
userID = testUser.ID
|
||||
|
||||
library1 = model.Library{ID: 500, Name: "Library 500", Path: "/path/500"}
|
||||
library2 = model.Library{ID: 501, Name: "Library 501", Path: "/path/501"}
|
||||
|
||||
// Create test libraries
|
||||
libRepo := NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
Expect(libRepo.Put(&library1)).To(BeNil())
|
||||
Expect(libRepo.Put(&library2)).To(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up user-library associations to ensure test isolation
|
||||
_ = repo.SetUserLibraries(userID, []int{})
|
||||
|
||||
// Clean up test libraries to ensure isolation between test groups
|
||||
libRepo := NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
_ = libRepo.(*libraryRepository).delete(squirrel.Eq{"id": []int{500, 501}})
|
||||
})
|
||||
|
||||
Describe("AddUserLibrary", func() {
|
||||
It("adds a library association for a user", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(library1.ID))
|
||||
})
|
||||
|
||||
It("does not create duplicate associations", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Add the same association again
|
||||
err = repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetUserLibraries", func() {
|
||||
It("returns empty list when user has no library associations", func() {
|
||||
libraries, err := repo.GetUserLibraries("non-existent-user")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
|
||||
It("returns user's associated libraries", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
err = repo.AddUserLibrary(userID, library2.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(2))
|
||||
|
||||
libIDs := []int{libraries[0].ID, libraries[1].ID}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("SetUserLibraries", func() {
|
||||
It("sets user's library associations", func() {
|
||||
libraryIDs := []int{library1.ID, library2.ID}
|
||||
err := repo.SetUserLibraries(userID, libraryIDs)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("replaces existing associations", func() {
|
||||
// Set initial associations
|
||||
err := repo.SetUserLibraries(userID, []int{library1.ID, library2.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Replace with just one library
|
||||
err = repo.SetUserLibraries(userID, []int{library1.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(library1.ID))
|
||||
})
|
||||
|
||||
It("removes all associations when passed empty slice", func() {
|
||||
// Set initial associations
|
||||
err := repo.SetUserLibraries(userID, []int{library1.ID, library2.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Remove all
|
||||
err = repo.SetUserLibraries(userID, []int{})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("RemoveUserLibrary", func() {
|
||||
It("removes a library association", func() {
|
||||
err := repo.AddUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
err = repo.AddUserLibrary(userID, library2.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
err = repo.RemoveUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(library2.ID))
|
||||
})
|
||||
|
||||
It("does nothing when association doesn't exist", func() {
|
||||
err := repo.RemoveUserLibrary(userID, library1.ID)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
libraries, err := repo.GetUserLibraries(userID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Admin User Auto-Assignment", func() {
|
||||
var (
|
||||
libRepo model.LibraryRepository
|
||||
library1 model.Library
|
||||
library2 model.Library
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
libRepo = NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
library1 = model.Library{ID: 201, Name: "Admin Test Library 1", Path: "/admin/test/path1"}
|
||||
library2 = model.Library{ID: 202, Name: "Admin Test Library 2", Path: "/admin/test/path2"}
|
||||
|
||||
// Create test libraries
|
||||
Expect(libRepo.Put(&library1)).To(BeNil())
|
||||
Expect(libRepo.Put(&library2)).To(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up test libraries and their associations
|
||||
_ = libRepo.(*libraryRepository).delete(squirrel.Eq{"id": []int{201, 202}})
|
||||
|
||||
// Clean up user-library associations for these test libraries
|
||||
_, _ = repo.(*userRepository).executeSQL(squirrel.Delete("user_library").Where(squirrel.Eq{"library_id": []int{201, 202}}))
|
||||
})
|
||||
|
||||
It("automatically assigns all libraries to admin users when created", func() {
|
||||
adminUser := model.User{
|
||||
ID: "admin-user-id-1",
|
||||
UserName: "adminuser1",
|
||||
Name: "Admin User",
|
||||
Email: "admin1@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
err := repo.Put(&adminUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Admin should automatically have access to all libraries (including existing ones)
|
||||
libraries, err := repo.GetUserLibraries(adminUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(3)) // Our 2 test libraries + existing library ID 1
|
||||
|
||||
libIDs := make([]int, len(libraries))
|
||||
for i, lib := range libraries {
|
||||
libIDs[i] = lib.ID
|
||||
}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1))
|
||||
})
|
||||
|
||||
It("automatically assigns all libraries to admin users when updated", func() {
|
||||
// Create regular user first
|
||||
regularUser := model.User{
|
||||
ID: "regular-user-id-1",
|
||||
UserName: "regularuser1",
|
||||
Name: "Regular User",
|
||||
Email: "regular1@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
err := repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Give them access to just one library
|
||||
err = repo.SetUserLibraries(regularUser.ID, []int{library1.ID})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Promote to admin
|
||||
regularUser.IsAdmin = true
|
||||
err = repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Should now have access to all libraries (including existing ones)
|
||||
libraries, err := repo.GetUserLibraries(regularUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(3)) // Our 2 test libraries + existing library ID 1
|
||||
|
||||
libIDs := make([]int, len(libraries))
|
||||
for i, lib := range libraries {
|
||||
libIDs[i] = lib.ID
|
||||
}
|
||||
// Should include our test libraries plus all existing ones
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1))
|
||||
})
|
||||
|
||||
It("does not affect regular users", func() {
|
||||
regularUser := model.User{
|
||||
ID: "regular-user-id-2",
|
||||
UserName: "regularuser2",
|
||||
Name: "Regular User",
|
||||
Email: "regular2@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
err := repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Regular user should have no library associations
|
||||
libraries, err := repo.GetUserLibraries(regularUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue