diff --git a/README.md b/README.md index 6bceba67c..f8e61abe1 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ $ make The server should start listening for requests. The default configuration is: - Port: `4533` -- User: `anyone` -- Password: `wordpass` +- User: `admin` +- Password: `admin` To override this or any other configuration, create a file named `sonic.toml` in the project folder. For all options see the [configuration.go](conf/configuration.go) file diff --git a/conf/configuration.go b/conf/configuration.go index a59f5de4b..edc70c7a3 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -15,8 +15,8 @@ type sonic struct { IgnoredArticles string `default:"The El La Los Las Le Les Os As O A"` IndexGroups string `default:"A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)"` - User string `default:"anyone"` - Password string `default:"wordpass"` + User string `default:"admin"` + Password string `default:"admin"` DisableDownsampling bool `default:"false"` DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"` diff --git a/engine/list_generator.go b/engine/list_generator.go index 8937778a0..4273f141a 100644 --- a/engine/list_generator.go +++ b/engine/list_generator.go @@ -31,42 +31,42 @@ type listGenerator struct { npRepo NowPlayingRepository } -// TODO: Only return albums that have the SortBy field != empty +// TODO: Only return albums that have the Sort field != empty func (g *listGenerator) query(qo model.QueryOptions, offset int, size int) (Entries, error) { qo.Offset = offset - qo.Size = size + qo.Max = size albums, err := g.ds.Album().GetAll(qo) return FromAlbums(albums), err } func (g *listGenerator) GetNewest(offset int, size int) (Entries, error) { - qo := model.QueryOptions{SortBy: "CreatedAt", Desc: true} + qo := model.QueryOptions{Sort: "CreatedAt", Order: "desc"} return g.query(qo, offset, size) } func (g *listGenerator) GetRecent(offset int, size int) (Entries, error) { - qo := model.QueryOptions{SortBy: "PlayDate", Desc: true} + qo := model.QueryOptions{Sort: "PlayDate", Order: "desc"} return g.query(qo, offset, size) } func (g *listGenerator) GetFrequent(offset int, size int) (Entries, error) { - qo := model.QueryOptions{SortBy: "PlayCount", Desc: true} + qo := model.QueryOptions{Sort: "PlayCount", Order: "desc"} return g.query(qo, offset, size) } func (g *listGenerator) GetHighest(offset int, size int) (Entries, error) { - qo := model.QueryOptions{SortBy: "Rating", Desc: true} + qo := model.QueryOptions{Sort: "Rating", Order: "desc"} return g.query(qo, offset, size) } func (g *listGenerator) GetByName(offset int, size int) (Entries, error) { - qo := model.QueryOptions{SortBy: "Name"} + qo := model.QueryOptions{Sort: "Name"} return g.query(qo, offset, size) } func (g *listGenerator) GetByArtist(offset int, size int) (Entries, error) { - qo := model.QueryOptions{SortBy: "Artist"} + qo := model.QueryOptions{Sort: "Artist"} return g.query(qo, offset, size) } @@ -111,7 +111,7 @@ func (g *listGenerator) GetRandomSongs(size int) (Entries, error) { } func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) { - qo := model.QueryOptions{Offset: offset, Size: size, SortBy: "starred_at", Desc: true} + qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"} albums, err := g.ds.Album().GetStarred(qo) if err != nil { return nil, err @@ -122,7 +122,7 @@ func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) { // TODO Return is confusing func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) { - artists, err := g.ds.Artist().GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true}) + artists, err := g.ds.Artist().GetStarred(model.QueryOptions{Sort: "starred_at", Order: "desc"}) if err != nil { return nil, nil, nil, err } @@ -132,7 +132,7 @@ func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) { return nil, nil, nil, err } - mediaFiles, err := g.ds.MediaFile().GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true}) + mediaFiles, err := g.ds.MediaFile().GetStarred(model.QueryOptions{Sort: "starred_at", Order: "desc"}) if err != nil { return nil, nil, nil, err } diff --git a/model/model.go b/model/model.go index a160ca580..9808356c8 100644 --- a/model/model.go +++ b/model/model.go @@ -14,14 +14,12 @@ var ( // Ex: var q = QueryOptions{Filters: Filters{"name__istartswith": "Deluan","age__gt": 25}} // All conditions will be ANDed together // TODO Implement filter in repositories' methods -type Filters map[string]interface{} - type QueryOptions struct { - SortBy string - Desc bool + Sort string + Order string + Max int Offset int - Size int - Filters Filters + Filters map[string]interface{} } type ResourceRepository interface { @@ -37,6 +35,7 @@ type DataStore interface { Genre() GenreRepository Playlist() PlaylistRepository Property() PropertyRepository + User() UserRepository Resource(model interface{}) ResourceRepository diff --git a/model/user.go b/model/user.go index 9bbca1a47..f2e2ab2bc 100644 --- a/model/user.go +++ b/model/user.go @@ -12,3 +12,9 @@ type User struct { CreatedAt time.Time UpdatedAt time.Time } + +type UserRepository interface { + CountAll(...QueryOptions) (int64, error) + Get(id string) (*User, error) + Put(*User) error +} diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go index 2624fdd70..fa3e0d55e 100644 --- a/persistence/album_repository_test.go +++ b/persistence/album_repository_test.go @@ -20,7 +20,7 @@ var _ = Describe("AlbumRepository", func() { }) It("returns all records sorted", func() { - Expect(repo.GetAll(model.QueryOptions{SortBy: "Name"})).To(Equal(model.Albums{ + Expect(repo.GetAll(model.QueryOptions{Sort: "Name"})).To(Equal(model.Albums{ albumAbbeyRoad, albumRadioactivity, albumSgtPeppers, @@ -28,7 +28,7 @@ var _ = Describe("AlbumRepository", func() { }) It("returns all records sorted desc", func() { - Expect(repo.GetAll(model.QueryOptions{SortBy: "Name", Desc: true})).To(Equal(model.Albums{ + Expect(repo.GetAll(model.QueryOptions{Sort: "Name", Order: "desc"})).To(Equal(model.Albums{ albumSgtPeppers, albumRadioactivity, albumAbbeyRoad, @@ -36,7 +36,7 @@ var _ = Describe("AlbumRepository", func() { }) It("paginates the result", func() { - Expect(repo.GetAll(model.QueryOptions{Offset: 1, Size: 1})).To(Equal(model.Albums{ + Expect(repo.GetAll(model.QueryOptions{Offset: 1, Max: 1})).To(Equal(model.Albums{ albumAbbeyRoad, })) }) diff --git a/persistence/mock_persistence.go b/persistence/mock_persistence.go index afb777f9b..37614047e 100644 --- a/persistence/mock_persistence.go +++ b/persistence/mock_persistence.go @@ -49,6 +49,10 @@ func (db *MockDataStore) Property() model.PropertyRepository { return struct{ model.PropertyRepository }{} } +func (db *MockDataStore) User() model.UserRepository { + return struct{ model.UserRepository }{} +} + func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error { return block(db) } diff --git a/persistence/persistence.go b/persistence/persistence.go index 63aa6f48a..67b37d638 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -69,8 +69,12 @@ func (db *SQLStore) Property() model.PropertyRepository { return NewPropertyRepository(db.getOrmer()) } +func (db *SQLStore) User() model.UserRepository { + return NewUserRepository(db.getOrmer()) +} + func (db *SQLStore) Resource(model interface{}) model.ResourceRepository { - return NewResource(db.getOrmer(), model, mappedModels[model]) + return NewResource(db.getOrmer(), model, getMappedModel(model)) } func (db *SQLStore) WithTx(block func(tx model.DataStore) error) error { @@ -129,19 +133,31 @@ func collectField(collection interface{}, getValue func(item interface{}) string return result } +func getType(myvar interface{}) string { + if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr { + return t.Elem().Name() + } else { + return t.Name() + } +} + func registerModel(model interface{}, mappedModel interface{}) { - mappedModels[model] = mappedModel + mappedModels[getType(model)] = mappedModel orm.RegisterModel(mappedModel) } +func getMappedModel(model interface{}) interface{} { + return mappedModels[getType(model)] +} + func init() { mappedModels = map[interface{}]interface{}{} - registerModel(new(model.Artist), new(artist)) - registerModel(new(model.Album), new(album)) - registerModel(new(model.MediaFile), new(mediaFile)) - registerModel(new(model.Property), new(property)) - registerModel(new(model.Playlist), new(playlist)) + registerModel(model.Artist{}, new(artist)) + registerModel(model.Album{}, new(album)) + registerModel(model.MediaFile{}, new(mediaFile)) + registerModel(model.Property{}, new(property)) + registerModel(model.Playlist{}, new(playlist)) registerModel(model.User{}, new(user)) orm.RegisterModel(new(checksum)) diff --git a/persistence/sql_repository.go b/persistence/sql_repository.go index 6e2008164..49801077e 100644 --- a/persistence/sql_repository.go +++ b/persistence/sql_repository.go @@ -16,14 +16,14 @@ func (r *sqlRepository) newQuery(options ...model.QueryOptions) orm.QuerySeter { if len(options) > 0 { opts := options[0] q = q.Offset(opts.Offset) - if opts.Size > 0 { - q = q.Limit(opts.Size) + if opts.Max > 0 { + q = q.Limit(opts.Max) } - if opts.SortBy != "" { - if opts.Desc { - q = q.OrderBy("-" + opts.SortBy) + if opts.Sort != "" { + if opts.Order == "desc" { + q = q.OrderBy("-" + opts.Sort) } else { - q = q.OrderBy(opts.SortBy) + q = q.OrderBy(opts.Sort) } } } diff --git a/persistence/user_repository.go b/persistence/user_repository.go index 3a4bef81e..71e736c52 100644 --- a/persistence/user_repository.go +++ b/persistence/user_repository.go @@ -3,13 +3,15 @@ package persistence import ( "time" + "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/model" + "github.com/deluan/rest" ) type user struct { ID string `json:"id" orm:"pk;column(id)"` Name string `json:"name" orm:"index"` - Password string `json:"-"` + Password string `json:"password"` IsAdmin bool `json:"isAdmin"` LastLoginAt *time.Time `json:"lastLoginAt" orm:"null"` LastAccessAt *time.Time `json:"lastAccessAt" orm:"null"` @@ -17,4 +19,45 @@ type user struct { UpdatedAt time.Time `json:"updatedAt" orm:"auto_now;type(datetime)"` } +type userRepository struct { + ormer orm.Ormer + userResource model.ResourceRepository +} + +func (r *userRepository) CountAll(qo ...model.QueryOptions) (int64, error) { + if len(qo) > 0 { + return r.userResource.Count(rest.QueryOptions(qo[0])) + } + return r.userResource.Count() +} + +func (r *userRepository) Get(id string) (*model.User, error) { + u, err := r.userResource.Read(id) + if err != nil { + return nil, err + } + res := model.User(u.(user)) + return &res, nil +} + +func (r *userRepository) Put(u *model.User) error { + c, err := r.CountAll() + if err != nil { + return err + } + if c == 0 { + mappedUser := user(*u) + _, err = r.userResource.Save(&mappedUser) + return err + } + return r.userResource.Update(u, "name", "is_admin", "password") +} + +func NewUserRepository(o orm.Ormer) model.UserRepository { + r := &userRepository{ormer: o} + r.userResource = NewResource(o, model.User{}, new(user)) + return r +} + var _ = model.User(user{}) +var _ model.UserRepository = (*userRepository)(nil) diff --git a/server/app/app.go b/server/app/app.go index db03ef196..ea25774c8 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -2,16 +2,21 @@ package app import ( "context" + "fmt" "net/http" "net/url" "strings" + "github.com/cloudsonic/sonic-server/log" "github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/server" "github.com/deluan/rest" "github.com/go-chi/chi" + "github.com/google/uuid" ) +const initialUser = "admin" + type Router struct { ds model.DataStore mux http.Handler @@ -21,6 +26,7 @@ type Router struct { func New(ds model.DataStore, path string) *Router { r := &Router{ds: ds, path: path} r.mux = r.routes() + r.createDefaultUser() return r } @@ -46,6 +52,24 @@ func (app *Router) routes() http.Handler { return r } +func (app *Router) createDefaultUser() { + c, err := app.ds.User().CountAll() + if err != nil { + panic(fmt.Sprintf("Could not access User table: %s", err)) + } + if c == 0 { + id, _ := uuid.NewRandom() + initialPassword, _ := uuid.NewRandom() + log.Warn("Creating initial user. Please change the password!", "user", initialUser, "password", initialPassword) + app.ds.User().Put(&model.User{ + ID: id.String(), + Name: initialUser, + Password: initialPassword.String(), + IsAdmin: true, + }) + } +} + func R(r chi.Router, pathPrefix string, newRepository rest.RepositoryConstructor) { r.Route(pathPrefix, func(r chi.Router) { r.Get("/", rest.GetAll(newRepository))