navidrome/model/user.go
Deluan ab2c77f7b2 feat(persistence): add UserRepository.UpdateImage
Adds the interface method, real repository implementation, and mock
together so the build stays green. UpdateImage sets updated_at
explicitly in the same UPDATE statement, since AvatarTag's ETag
derives from uploaded_image + updated_at.
2026-09-09 18:24:15 -04:00

89 lines
3.2 KiB
Go

package model
import (
"crypto/md5"
"encoding/hex"
"fmt"
"time"
"github.com/navidrome/navidrome/consts"
)
type User struct {
ID string `structs:"id" json:"id"`
UserName string `structs:"user_name" json:"userName"`
Name string `structs:"name" json:"name"`
Email string `structs:"email" json:"email"`
IsAdmin bool `structs:"is_admin" json:"isAdmin"`
LastLoginAt *time.Time `structs:"last_login_at" json:"lastLoginAt"`
LastAccessAt *time.Time `structs:"last_access_at" json:"lastAccessAt"`
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
// Smart-playlist criteria JSON; matching songs are not sent to external scrobblers
ScrobbleFilter string `structs:"scrobble_filter" json:"scrobbleFilter"`
// structs:"-" because userRepository.Put writes every mapped column with no column list, so a
// profile save from the UI would blank this. UpdateImage is the only writer.
UploadedImage string `structs:"-" json:"uploadedImage,omitempty"`
// Library associations (many-to-many relationship)
Libraries Libraries `structs:"-" json:"libraries,omitempty"`
// This is only available on the backend, and it is never sent over the wire
Password string `structs:"-" json:"-"`
// Bumped on password change to invalidate every issued token for this user.
TokenEpoch int `structs:"-" json:"-"`
// This is used to set or change a password when calling Put. If it is empty, the password is not changed.
// It is received from the UI with the name "password"
NewPassword string `structs:"password,omitempty" json:"password,omitempty"` //nolint:gosec
// If changing the password, this is also required
CurrentPassword string `structs:"current_password,omitempty" json:"currentPassword,omitempty"`
}
func (u User) HasLibraryAccess(libraryID int) bool {
if u.IsAdmin {
return true // Admin users have access to all libraries
}
for _, lib := range u.Libraries {
if lib.ID == libraryID {
return true
}
}
return false
}
func (u User) UploadedImagePath() string {
return UploadedImagePath(consts.EntityUser, u.UploadedImage)
}
func (u User) AvatarTag() string {
if u.UploadedImage == "" {
return ""
}
sum := md5.Sum(fmt.Appendf(nil, "%s|%d", u.UploadedImage, u.UpdatedAt.UnixNano()))
return hex.EncodeToString(sum[:])[:16]
}
type Users []User
type UserRepository interface {
ResourceRepository
CountAll(...QueryOptions) (int64, error)
Delete(id string) error
Get(id string) (*User, error)
GetAll(options ...QueryOptions) (Users, error)
Put(*User) error
// UpdateImage is the only writer of uploaded_image. See the field comment on User.
UpdateImage(id string, filename string) error
UpdateLastLoginAt(id string) error
UpdateLastAccessAt(id string) error
FindFirstAdmin() (*User, error)
// FindByUsername must be case-insensitive
FindByUsername(username string) (*User, error)
// FindByUsernameWithPassword is the same as above, but also returns the decrypted password
FindByUsernameWithPassword(username string) (*User, error)
// Library association methods
GetUserLibraries(userID string) (Libraries, error)
SetUserLibraries(userID string, libraryIDs []int) error
}