Create initial user if User table is empty. Also make model.QueryOptions compatible with rest.QueryOptions

This commit is contained in:
Deluan 2020-01-19 22:36:15 -05:00
parent 2ab0cecd48
commit 491bfb1f69
11 changed files with 130 additions and 38 deletions

View file

@ -38,8 +38,8 @@ $ make
The server should start listening for requests. The default configuration is: The server should start listening for requests. The default configuration is:
- Port: `4533` - Port: `4533`
- User: `anyone` - User: `admin`
- Password: `wordpass` - Password: `admin`
To override this or any other configuration, create a file named `sonic.toml` in the project folder. 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 For all options see the [configuration.go](conf/configuration.go) file

View file

@ -15,8 +15,8 @@ type sonic struct {
IgnoredArticles string `default:"The El La Los Las Le Les Os As O A"` 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]([)"` 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"` User string `default:"admin"`
Password string `default:"wordpass"` Password string `default:"admin"`
DisableDownsampling bool `default:"false"` DisableDownsampling bool `default:"false"`
DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"` DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"`

View file

@ -31,42 +31,42 @@ type listGenerator struct {
npRepo NowPlayingRepository 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) { func (g *listGenerator) query(qo model.QueryOptions, offset int, size int) (Entries, error) {
qo.Offset = offset qo.Offset = offset
qo.Size = size qo.Max = size
albums, err := g.ds.Album().GetAll(qo) albums, err := g.ds.Album().GetAll(qo)
return FromAlbums(albums), err return FromAlbums(albums), err
} }
func (g *listGenerator) GetNewest(offset int, size int) (Entries, error) { 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) return g.query(qo, offset, size)
} }
func (g *listGenerator) GetRecent(offset int, size int) (Entries, error) { 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) return g.query(qo, offset, size)
} }
func (g *listGenerator) GetFrequent(offset int, size int) (Entries, error) { 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) return g.query(qo, offset, size)
} }
func (g *listGenerator) GetHighest(offset int, size int) (Entries, error) { 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) return g.query(qo, offset, size)
} }
func (g *listGenerator) GetByName(offset int, size int) (Entries, error) { 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) return g.query(qo, offset, size)
} }
func (g *listGenerator) GetByArtist(offset int, size int) (Entries, error) { 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) 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) { 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) albums, err := g.ds.Album().GetStarred(qo)
if err != nil { if err != nil {
return nil, err return nil, err
@ -122,7 +122,7 @@ func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) {
// TODO Return is confusing // TODO Return is confusing
func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) { 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 { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@ -132,7 +132,7 @@ func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) {
return nil, nil, nil, err 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 { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }

View file

@ -14,14 +14,12 @@ var (
// Ex: var q = QueryOptions{Filters: Filters{"name__istartswith": "Deluan","age__gt": 25}} // Ex: var q = QueryOptions{Filters: Filters{"name__istartswith": "Deluan","age__gt": 25}}
// All conditions will be ANDed together // All conditions will be ANDed together
// TODO Implement filter in repositories' methods // TODO Implement filter in repositories' methods
type Filters map[string]interface{}
type QueryOptions struct { type QueryOptions struct {
SortBy string Sort string
Desc bool Order string
Max int
Offset int Offset int
Size int Filters map[string]interface{}
Filters Filters
} }
type ResourceRepository interface { type ResourceRepository interface {
@ -37,6 +35,7 @@ type DataStore interface {
Genre() GenreRepository Genre() GenreRepository
Playlist() PlaylistRepository Playlist() PlaylistRepository
Property() PropertyRepository Property() PropertyRepository
User() UserRepository
Resource(model interface{}) ResourceRepository Resource(model interface{}) ResourceRepository

View file

@ -12,3 +12,9 @@ type User struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
type UserRepository interface {
CountAll(...QueryOptions) (int64, error)
Get(id string) (*User, error)
Put(*User) error
}

View file

@ -20,7 +20,7 @@ var _ = Describe("AlbumRepository", func() {
}) })
It("returns all records sorted", 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, albumAbbeyRoad,
albumRadioactivity, albumRadioactivity,
albumSgtPeppers, albumSgtPeppers,
@ -28,7 +28,7 @@ var _ = Describe("AlbumRepository", func() {
}) })
It("returns all records sorted desc", 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, albumSgtPeppers,
albumRadioactivity, albumRadioactivity,
albumAbbeyRoad, albumAbbeyRoad,
@ -36,7 +36,7 @@ var _ = Describe("AlbumRepository", func() {
}) })
It("paginates the result", 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, albumAbbeyRoad,
})) }))
}) })

View file

@ -49,6 +49,10 @@ func (db *MockDataStore) Property() model.PropertyRepository {
return struct{ 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 { func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error {
return block(db) return block(db)
} }

View file

@ -69,8 +69,12 @@ func (db *SQLStore) Property() model.PropertyRepository {
return NewPropertyRepository(db.getOrmer()) return NewPropertyRepository(db.getOrmer())
} }
func (db *SQLStore) User() model.UserRepository {
return NewUserRepository(db.getOrmer())
}
func (db *SQLStore) Resource(model interface{}) model.ResourceRepository { 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 { 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 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{}) { func registerModel(model interface{}, mappedModel interface{}) {
mappedModels[model] = mappedModel mappedModels[getType(model)] = mappedModel
orm.RegisterModel(mappedModel) orm.RegisterModel(mappedModel)
} }
func getMappedModel(model interface{}) interface{} {
return mappedModels[getType(model)]
}
func init() { func init() {
mappedModels = map[interface{}]interface{}{} mappedModels = map[interface{}]interface{}{}
registerModel(new(model.Artist), new(artist)) registerModel(model.Artist{}, new(artist))
registerModel(new(model.Album), new(album)) registerModel(model.Album{}, new(album))
registerModel(new(model.MediaFile), new(mediaFile)) registerModel(model.MediaFile{}, new(mediaFile))
registerModel(new(model.Property), new(property)) registerModel(model.Property{}, new(property))
registerModel(new(model.Playlist), new(playlist)) registerModel(model.Playlist{}, new(playlist))
registerModel(model.User{}, new(user)) registerModel(model.User{}, new(user))
orm.RegisterModel(new(checksum)) orm.RegisterModel(new(checksum))

View file

@ -16,14 +16,14 @@ func (r *sqlRepository) newQuery(options ...model.QueryOptions) orm.QuerySeter {
if len(options) > 0 { if len(options) > 0 {
opts := options[0] opts := options[0]
q = q.Offset(opts.Offset) q = q.Offset(opts.Offset)
if opts.Size > 0 { if opts.Max > 0 {
q = q.Limit(opts.Size) q = q.Limit(opts.Max)
} }
if opts.SortBy != "" { if opts.Sort != "" {
if opts.Desc { if opts.Order == "desc" {
q = q.OrderBy("-" + opts.SortBy) q = q.OrderBy("-" + opts.Sort)
} else { } else {
q = q.OrderBy(opts.SortBy) q = q.OrderBy(opts.Sort)
} }
} }
} }

View file

@ -3,13 +3,15 @@ package persistence
import ( import (
"time" "time"
"github.com/astaxie/beego/orm"
"github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/model"
"github.com/deluan/rest"
) )
type user struct { type user struct {
ID string `json:"id" orm:"pk;column(id)"` ID string `json:"id" orm:"pk;column(id)"`
Name string `json:"name" orm:"index"` Name string `json:"name" orm:"index"`
Password string `json:"-"` Password string `json:"password"`
IsAdmin bool `json:"isAdmin"` IsAdmin bool `json:"isAdmin"`
LastLoginAt *time.Time `json:"lastLoginAt" orm:"null"` LastLoginAt *time.Time `json:"lastLoginAt" orm:"null"`
LastAccessAt *time.Time `json:"lastAccessAt" 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)"` 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.User(user{})
var _ model.UserRepository = (*userRepository)(nil)

View file

@ -2,16 +2,21 @@ package app
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/cloudsonic/sonic-server/log"
"github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/model"
"github.com/cloudsonic/sonic-server/server" "github.com/cloudsonic/sonic-server/server"
"github.com/deluan/rest" "github.com/deluan/rest"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/google/uuid"
) )
const initialUser = "admin"
type Router struct { type Router struct {
ds model.DataStore ds model.DataStore
mux http.Handler mux http.Handler
@ -21,6 +26,7 @@ type Router struct {
func New(ds model.DataStore, path string) *Router { func New(ds model.DataStore, path string) *Router {
r := &Router{ds: ds, path: path} r := &Router{ds: ds, path: path}
r.mux = r.routes() r.mux = r.routes()
r.createDefaultUser()
return r return r
} }
@ -46,6 +52,24 @@ func (app *Router) routes() http.Handler {
return r 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) { func R(r chi.Router, pathPrefix string, newRepository rest.RepositoryConstructor) {
r.Route(pathPrefix, func(r chi.Router) { r.Route(pathPrefix, func(r chi.Router) {
r.Get("/", rest.GetAll(newRepository)) r.Get("/", rest.GetAll(newRepository))