mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Compare commits
8 Commits
451f0795ba
...
42890bc526
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42890bc526 | ||
|
|
e36fef8692 | ||
|
|
9913235542 | ||
|
|
a87b6a50a6 | ||
|
|
2b30ed1520 | ||
|
|
1024d61a5e | ||
|
|
bbe8fe164d | ||
|
|
72969711d2 |
@ -113,9 +113,9 @@ func WithAdminUser(ctx context.Context, ds model.DataStore) context.Context {
|
||||
if err != nil {
|
||||
c, err := ds.User(ctx).CountAll()
|
||||
if c == 0 && err == nil {
|
||||
log.Debug(ctx, "Scanner: No admin user yet!", err)
|
||||
log.Debug(ctx, "No admin user yet!", err)
|
||||
} else {
|
||||
log.Error(ctx, "Scanner: No admin user found!", err)
|
||||
log.Error(ctx, "No admin user found!", err)
|
||||
}
|
||||
u = &model.User{}
|
||||
}
|
||||
|
||||
@ -130,58 +130,99 @@ func (s *Stream) EstimatedContentLength() int {
|
||||
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
|
||||
}
|
||||
|
||||
// TODO This function deserves some love (refactoring)
|
||||
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
|
||||
// Default case
|
||||
format = "raw"
|
||||
bitRate = 0
|
||||
|
||||
// If the client explicitly requests "raw"
|
||||
// then always serve the original
|
||||
if reqFormat == "raw" {
|
||||
return format, bitRate
|
||||
}
|
||||
|
||||
// If requested format matches the file’s suffix and
|
||||
// no bitrate reduction is requested then
|
||||
// stream the file without transcoding
|
||||
if reqFormat == mf.Suffix && reqBitRate == 0 {
|
||||
return format, mf.BitRate
|
||||
}
|
||||
|
||||
targetFormat, targetBitRate := findTargetTranscodingOptions(ctx, mf, reqFormat, reqBitRate)
|
||||
|
||||
// If nothing was found then stream raw
|
||||
if targetFormat == "" && targetBitRate == 0 {
|
||||
return format, 0
|
||||
}
|
||||
if reqFormat == mf.Suffix && reqBitRate == 0 {
|
||||
bitRate = mf.BitRate
|
||||
return format, bitRate
|
||||
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(targetFormat)
|
||||
if err != nil {
|
||||
// TODO: log error?
|
||||
return format, 0
|
||||
}
|
||||
trc, hasDefault := request.TranscodingFrom(ctx)
|
||||
var cFormat string
|
||||
var cBitRate int
|
||||
if reqFormat != "" {
|
||||
cFormat = reqFormat
|
||||
|
||||
format = t.TargetFormat
|
||||
|
||||
// If no target bitrate was specified
|
||||
// fall back to the transcoding’s configuration
|
||||
// default bitrate
|
||||
if targetBitRate == 0 {
|
||||
bitRate = t.DefaultBitRate
|
||||
} else {
|
||||
if hasDefault {
|
||||
cFormat = trc.TargetFormat
|
||||
cBitRate = trc.DefaultBitRate
|
||||
if p, ok := request.PlayerFrom(ctx); ok {
|
||||
cBitRate = p.MaxBitRate
|
||||
}
|
||||
} else if reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "" {
|
||||
// If no format is specified and no transcoding associated to the player, but a bitrate is specified,
|
||||
// and there is no transcoding set for the player, we use the default downsampling format.
|
||||
// But only if the requested bitRate is lower than the original bitRate.
|
||||
log.Debug("Default Downsampling", "Using default downsampling format", conf.Server.DefaultDownsamplingFormat)
|
||||
cFormat = conf.Server.DefaultDownsamplingFormat
|
||||
}
|
||||
}
|
||||
if reqBitRate > 0 {
|
||||
cBitRate = reqBitRate
|
||||
}
|
||||
if cBitRate == 0 && cFormat == "" {
|
||||
return format, bitRate
|
||||
}
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
|
||||
if err == nil {
|
||||
format = t.TargetFormat
|
||||
if cBitRate != 0 {
|
||||
bitRate = cBitRate
|
||||
} else {
|
||||
bitRate = t.DefaultBitRate
|
||||
}
|
||||
bitRate = targetBitRate
|
||||
}
|
||||
|
||||
// If the final format is the same as the original
|
||||
// and does not reduce bitrate
|
||||
// there’s no reason to transcode
|
||||
if format == mf.Suffix && bitRate >= mf.BitRate {
|
||||
format = "raw"
|
||||
bitRate = 0
|
||||
return "raw", 0
|
||||
}
|
||||
|
||||
return format, bitRate
|
||||
}
|
||||
|
||||
func findTargetTranscodingOptions(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (string, int) {
|
||||
// If a format is requested use that
|
||||
if reqFormat != "" {
|
||||
return reqFormat, reqBitRate
|
||||
}
|
||||
|
||||
// If a default transcoding configuration exists for this context
|
||||
if trc, ok := request.TranscodingFrom(ctx); ok {
|
||||
targetFormat := trc.TargetFormat
|
||||
targetBitRate := trc.DefaultBitRate
|
||||
|
||||
// If a player is configured adjust bitrate based on
|
||||
// user request or player limits
|
||||
if p, hasPlayer := request.PlayerFrom(ctx); hasPlayer {
|
||||
if reqBitRate > 0 {
|
||||
targetBitRate = reqBitRate
|
||||
} else if p.MaxBitRate > 0 {
|
||||
targetBitRate = p.MaxBitRate
|
||||
}
|
||||
} else if reqBitRate > 0 {
|
||||
targetBitRate = reqBitRate
|
||||
}
|
||||
|
||||
return targetFormat, targetBitRate
|
||||
}
|
||||
|
||||
// Use the default downsampling format the server is configured to but
|
||||
// only if the requested bitrate is reduced
|
||||
isBitrateReduced := reqBitRate > 0 && reqBitRate < mf.BitRate
|
||||
hasDefaultDownsamplingFormat := conf.Server.DefaultDownsamplingFormat != ""
|
||||
|
||||
if isBitrateReduced && hasDefaultDownsamplingFormat {
|
||||
log.Debug("Default Downsampling",
|
||||
"Using default downsampling format",
|
||||
conf.Server.DefaultDownsamplingFormat)
|
||||
return conf.Server.DefaultDownsamplingFormat, reqBitRate
|
||||
}
|
||||
|
||||
return "", 0
|
||||
}
|
||||
|
||||
var (
|
||||
onceTranscodingCache sync.Once
|
||||
instanceTranscodingCache TranscodingCache
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/metrics/insights"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/plugins/schema"
|
||||
"github.com/navidrome/navidrome/utils/singleton"
|
||||
)
|
||||
@ -64,9 +65,16 @@ func GetInstance(ds model.DataStore, pluginLoader PluginLoader) Insights {
|
||||
}
|
||||
|
||||
func (c *insightsCollector) Run(ctx context.Context) {
|
||||
ctx = auth.WithAdminUser(ctx, c.ds)
|
||||
for {
|
||||
c.sendInsights(ctx)
|
||||
// Refresh admin context on each iteration to handle cases where
|
||||
// admin user wasn't available on previous runs
|
||||
insightsCtx := auth.WithAdminUser(ctx, c.ds)
|
||||
u, _ := request.UserFrom(insightsCtx)
|
||||
if !u.IsAdmin {
|
||||
log.Trace(insightsCtx, "No admin user available, skipping insights collection")
|
||||
} else {
|
||||
c.sendInsights(insightsCtx)
|
||||
}
|
||||
select {
|
||||
case <-time.After(consts.InsightsUpdateInterval):
|
||||
continue
|
||||
|
||||
@ -264,6 +264,11 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
||||
"annotation.item_id = media_file.id" +
|
||||
" AND annotation.item_type = 'media_file'" +
|
||||
" AND annotation.user_id = '" + usr.ID + "')")
|
||||
|
||||
// Only include media files from libraries the user has access to
|
||||
sq = r.applyLibraryFilter(sq, "media_file")
|
||||
|
||||
// Apply the criteria rules
|
||||
sq = r.addCriteria(sq, rules)
|
||||
insSql := Insert("playlist_tracks").Columns("id", "playlist_id", "media_file_id").Select(sq)
|
||||
_, err = r.executeSQL(insSql)
|
||||
|
||||
@ -366,4 +366,136 @@ var _ = Describe("PlaylistRepository", func() {
|
||||
Expect(foundWithoutGrouping).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Smart Playlists Library Filtering", func() {
|
||||
var mfRepo model.MediaFileRepository
|
||||
var testPlaylistID string
|
||||
var lib2ID int
|
||||
var restrictedUserID string
|
||||
var uniqueLibPath string
|
||||
|
||||
BeforeEach(func() {
|
||||
db := GetDBXBuilder()
|
||||
|
||||
// Generate unique IDs for this test run
|
||||
uniqueSuffix := time.Now().Format("20060102150405.000")
|
||||
restrictedUserID = "restricted-user-" + uniqueSuffix
|
||||
uniqueLibPath = "/music/lib2-" + uniqueSuffix
|
||||
|
||||
// Create a second library with unique name and path to avoid conflicts with other tests
|
||||
_, err := db.DB().Exec("INSERT INTO library (name, path, created_at, updated_at) VALUES (?, ?, datetime('now'), datetime('now'))", "Library 2-"+uniqueSuffix, uniqueLibPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = db.DB().QueryRow("SELECT last_insert_rowid()").Scan(&lib2ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create a restricted user with access only to library 1
|
||||
_, err = db.DB().Exec("INSERT INTO user (id, user_name, name, is_admin, password, created_at, updated_at) VALUES (?, ?, 'Restricted User', false, 'pass', datetime('now'), datetime('now'))", restrictedUserID, restrictedUserID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = db.DB().Exec("INSERT INTO user_library (user_id, library_id) VALUES (?, 1)", restrictedUserID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create test media files in each library
|
||||
ctx := log.NewContext(GinkgoT().Context())
|
||||
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid", IsAdmin: true})
|
||||
mfRepo = NewMediaFileRepository(ctx, db)
|
||||
|
||||
// Song in library 1 (accessible by restricted user)
|
||||
songLib1 := model.MediaFile{
|
||||
ID: "lib1-song",
|
||||
Title: "Song in Lib1",
|
||||
Artist: "Test Artist",
|
||||
ArtistID: "1",
|
||||
Album: "Test Album",
|
||||
AlbumID: "101",
|
||||
Path: "/music/lib1/song.mp3",
|
||||
LibraryID: 1,
|
||||
Participants: model.Participants{},
|
||||
Tags: model.Tags{},
|
||||
Lyrics: "[]",
|
||||
}
|
||||
Expect(mfRepo.Put(&songLib1)).To(Succeed())
|
||||
|
||||
// Song in library 2 (NOT accessible by restricted user)
|
||||
songLib2 := model.MediaFile{
|
||||
ID: "lib2-song",
|
||||
Title: "Song in Lib2",
|
||||
Artist: "Test Artist",
|
||||
ArtistID: "1",
|
||||
Album: "Test Album",
|
||||
AlbumID: "101",
|
||||
Path: uniqueLibPath + "/song.mp3",
|
||||
LibraryID: lib2ID,
|
||||
Participants: model.Participants{},
|
||||
Tags: model.Tags{},
|
||||
Lyrics: "[]",
|
||||
}
|
||||
Expect(mfRepo.Put(&songLib2)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
db := GetDBXBuilder()
|
||||
if testPlaylistID != "" {
|
||||
_ = repo.Delete(testPlaylistID)
|
||||
testPlaylistID = ""
|
||||
}
|
||||
// Clean up test data
|
||||
_, _ = db.Delete("media_file", dbx.HashExp{"id": "lib1-song"}).Execute()
|
||||
_, _ = db.Delete("media_file", dbx.HashExp{"id": "lib2-song"}).Execute()
|
||||
_, _ = db.Delete("user_library", dbx.HashExp{"user_id": restrictedUserID}).Execute()
|
||||
_, _ = db.Delete("user", dbx.HashExp{"id": restrictedUserID}).Execute()
|
||||
_, _ = db.DB().Exec("DELETE FROM library WHERE id = ?", lib2ID)
|
||||
})
|
||||
|
||||
It("should only include tracks from libraries the user has access to (issue #4738)", func() {
|
||||
db := GetDBXBuilder()
|
||||
ctx := log.NewContext(GinkgoT().Context())
|
||||
|
||||
// Create the smart playlist as the restricted user
|
||||
restrictedUser := model.User{ID: restrictedUserID, UserName: restrictedUserID, IsAdmin: false}
|
||||
ctx = request.WithUser(ctx, restrictedUser)
|
||||
restrictedRepo := NewPlaylistRepository(ctx, db)
|
||||
|
||||
// Create a smart playlist that matches all songs
|
||||
rules := &criteria.Criteria{
|
||||
Expression: criteria.All{
|
||||
criteria.Gt{"playCount": -1}, // Matches everything
|
||||
},
|
||||
}
|
||||
newPls := model.Playlist{Name: "All Songs", OwnerID: restrictedUserID, Rules: rules}
|
||||
Expect(restrictedRepo.Put(&newPls)).To(Succeed())
|
||||
testPlaylistID = newPls.ID
|
||||
|
||||
By("refreshing the smart playlist")
|
||||
conf.Server.SmartPlaylistRefreshDelay = -1 * time.Second // Force refresh
|
||||
pls, err := restrictedRepo.GetWithTracks(newPls.ID, true, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
By("verifying only the track from library 1 is in the playlist")
|
||||
var foundLib1Song, foundLib2Song bool
|
||||
for _, track := range pls.Tracks {
|
||||
if track.MediaFileID == "lib1-song" {
|
||||
foundLib1Song = true
|
||||
}
|
||||
if track.MediaFileID == "lib2-song" {
|
||||
foundLib2Song = true
|
||||
}
|
||||
}
|
||||
Expect(foundLib1Song).To(BeTrue(), "Song from library 1 should be in the playlist")
|
||||
Expect(foundLib2Song).To(BeFalse(), "Song from library 2 should NOT be in the playlist")
|
||||
|
||||
By("verifying playlist_tracks table only contains the accessible track")
|
||||
var playlistTracksCount int
|
||||
err = db.DB().QueryRow("SELECT count(*) FROM playlist_tracks WHERE playlist_id = ?", newPls.ID).Scan(&playlistTracksCount)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Count should only include tracks visible to the user (lib1-song)
|
||||
// The count may include other test songs from library 1, but NOT lib2-song
|
||||
var lib2TrackCount int
|
||||
err = db.DB().QueryRow("SELECT count(*) FROM playlist_tracks WHERE playlist_id = ? AND media_file_id = 'lib2-song'", newPls.ID).Scan(&lib2TrackCount)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lib2TrackCount).To(Equal(0), "lib2-song should not be in playlist_tracks")
|
||||
|
||||
By("verifying SongCount matches visible tracks")
|
||||
Expect(pls.SongCount).To(Equal(len(pls.Tracks)), "SongCount should match the number of visible tracks")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
@ -45,6 +46,9 @@ var _ = Describe("Tag Library Filtering", func() {
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
|
||||
// Generate unique path suffix to avoid conflicts with other tests
|
||||
uniqueSuffix := time.Now().Format("20060102150405.000")
|
||||
|
||||
// Clean up database
|
||||
db := GetDBXBuilder()
|
||||
_, err := db.NewQuery("DELETE FROM library_tag").Execute()
|
||||
@ -57,12 +61,12 @@ var _ = Describe("Tag Library Filtering", func() {
|
||||
_, err = db.NewQuery("DELETE FROM library WHERE id > 1").Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create test libraries
|
||||
// Create test libraries with unique names and paths to avoid conflicts with other tests
|
||||
_, err = db.NewQuery("INSERT INTO library (id, name, path) VALUES ({:id}, {:name}, {:path})").
|
||||
Bind(dbx.Params{"id": libraryID2, "name": "Library 2", "path": "/music/lib2"}).Execute()
|
||||
Bind(dbx.Params{"id": libraryID2, "name": "Library 2-" + uniqueSuffix, "path": "/music/lib2-" + uniqueSuffix}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = db.NewQuery("INSERT INTO library (id, name, path) VALUES ({:id}, {:name}, {:path})").
|
||||
Bind(dbx.Params{"id": libraryID3, "name": "Library 3", "path": "/music/lib3"}).Execute()
|
||||
Bind(dbx.Params{"id": libraryID3, "name": "Library 3-" + uniqueSuffix, "path": "/music/lib3-" + uniqueSuffix}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Give admin access to all libraries
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@ -69,6 +72,13 @@ func (s *Server) Run(ctx context.Context, addr string, port int, tlsCert string,
|
||||
// Determine if TLS is enabled
|
||||
tlsEnabled := tlsCert != "" && tlsKey != ""
|
||||
|
||||
// Validate TLS certificates before starting the server
|
||||
if tlsEnabled {
|
||||
if err := validateTLSCertificates(tlsCert, tlsKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a listener based on the address type (either Unix socket or TCP)
|
||||
var listener net.Listener
|
||||
var err error
|
||||
@ -89,17 +99,17 @@ func (s *Server) Run(ctx context.Context, addr string, port int, tlsCert string,
|
||||
// Start the server in a new goroutine and send an error signal to errC if there's an error
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
var err error
|
||||
if tlsEnabled {
|
||||
// Start the HTTPS server
|
||||
log.Info("Starting server with TLS (HTTPS) enabled", "tlsCert", tlsCert, "tlsKey", tlsKey)
|
||||
if err := server.ServeTLS(listener, tlsCert, tlsKey); !errors.Is(err, http.ErrServerClosed) {
|
||||
errC <- err
|
||||
}
|
||||
err = server.ServeTLS(listener, tlsCert, tlsKey)
|
||||
} else {
|
||||
// Start the HTTP server
|
||||
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||
errC <- err
|
||||
}
|
||||
err = server.Serve(listener)
|
||||
}
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
errC <- err
|
||||
}
|
||||
}()
|
||||
|
||||
@ -249,3 +259,56 @@ func AbsoluteURL(r *http.Request, u string, params url.Values) string {
|
||||
}
|
||||
return buildUrl.String()
|
||||
}
|
||||
|
||||
// validateTLSCertificates validates the TLS certificate and key files before starting the server.
|
||||
// It provides detailed error messages for common issues like encrypted private keys.
|
||||
func validateTLSCertificates(certFile, keyFile string) error {
|
||||
// Read the key file to check for encryption
|
||||
keyData, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading TLS key file: %w", err)
|
||||
}
|
||||
|
||||
// Parse PEM blocks and check for encryption
|
||||
block, _ := pem.Decode(keyData)
|
||||
if block == nil {
|
||||
return errors.New("TLS key file does not contain a valid PEM block")
|
||||
}
|
||||
|
||||
// Check for encrypted private key indicators
|
||||
if isEncryptedPEM(block, keyData) {
|
||||
return errors.New("TLS private key is encrypted (password-protected). " +
|
||||
"Navidrome does not support encrypted private keys. " +
|
||||
"Please decrypt your key using: openssl pkey -in <encrypted-key> -out <decrypted-key>")
|
||||
}
|
||||
|
||||
// Try to load the certificate pair to validate it
|
||||
_, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading TLS certificate/key pair: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isEncryptedPEM checks if a PEM block represents an encrypted private key.
|
||||
func isEncryptedPEM(block *pem.Block, rawData []byte) bool {
|
||||
// Check for PKCS#8 encrypted format (BEGIN ENCRYPTED PRIVATE KEY)
|
||||
if block.Type == "ENCRYPTED PRIVATE KEY" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for legacy encrypted format with Proc-Type header
|
||||
if block.Headers != nil {
|
||||
if procType, ok := block.Headers["Proc-Type"]; ok && strings.Contains(procType, "ENCRYPTED") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Also check raw data for DEK-Info header (in case pem.Decode doesn't parse headers correctly)
|
||||
if bytes.Contains(rawData, []byte("DEK-Info:")) || bytes.Contains(rawData, []byte("Proc-Type: 4,ENCRYPTED")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@ -107,3 +114,146 @@ var _ = Describe("createUnixSocketFile", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("TLS support", func() {
|
||||
Describe("validateTLSCertificates", func() {
|
||||
const testDataDir = "server/testdata"
|
||||
|
||||
When("certificate and key are valid and unencrypted", func() {
|
||||
It("returns nil", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
When("private key is encrypted with PKCS#8 format", func() {
|
||||
It("returns an error with helpful message", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert_encrypted.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key_encrypted.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("encrypted"))
|
||||
Expect(err.Error()).To(ContainSubstring("openssl"))
|
||||
})
|
||||
})
|
||||
|
||||
When("private key is encrypted with legacy format (Proc-Type header)", func() {
|
||||
It("returns an error with helpful message", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key_encrypted_legacy.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("encrypted"))
|
||||
Expect(err.Error()).To(ContainSubstring("openssl"))
|
||||
})
|
||||
})
|
||||
|
||||
When("key file does not exist", func() {
|
||||
It("returns an error", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "nonexistent.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("reading TLS key file"))
|
||||
})
|
||||
})
|
||||
|
||||
When("key file does not contain valid PEM", func() {
|
||||
It("returns an error", func() {
|
||||
// Create a temp file with invalid PEM content
|
||||
tmpFile, err := os.CreateTemp("", "invalid_key*.pem")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
DeferCleanup(func() {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
})
|
||||
_, err = tmpFile.WriteString("not a valid PEM file")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_ = tmpFile.Close()
|
||||
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
err = validateTLSCertificates(certFile, tmpFile.Name())
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("valid PEM block"))
|
||||
})
|
||||
})
|
||||
|
||||
When("certificate file does not exist", func() {
|
||||
It("returns an error from tls.LoadX509KeyPair", func() {
|
||||
certFile := filepath.Join(testDataDir, "nonexistent_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("loading TLS certificate/key pair"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Server TLS", func() {
|
||||
const testDataDir = "server/testdata"
|
||||
|
||||
When("server is started with valid TLS certificates", func() {
|
||||
It("accepts HTTPS connections", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
|
||||
// Create server with mock dependencies
|
||||
ds := &tests.MockDataStore{}
|
||||
server := New(ds, nil, nil)
|
||||
|
||||
// Load the test certificate to create a trusted CA pool
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key.pem")
|
||||
caCert, err := os.ReadFile(certFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
// Create an HTTPS client that trusts our test certificate
|
||||
httpClient := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Start the server in a goroutine
|
||||
ctx, cancel := context.WithCancel(GinkgoT().Context())
|
||||
defer cancel()
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
errChan <- server.Run(ctx, "127.0.0.1", 14534, certFile, keyFile)
|
||||
}()
|
||||
|
||||
Eventually(func() error {
|
||||
// Make an HTTPS request to the server
|
||||
resp, err := httpClient.Get("https://127.0.0.1:14534/ping")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}, 2*time.Second, 100*time.Millisecond).Should(Succeed())
|
||||
|
||||
// Stop the server
|
||||
cancel()
|
||||
|
||||
// Wait for server to stop (with timeout)
|
||||
select {
|
||||
case <-errChan:
|
||||
// Server stopped
|
||||
case <-time.After(2 * time.Second):
|
||||
Fail("Server did not stop in time")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
23
server/testdata/test_cert.pem
vendored
Normal file
23
server/testdata/test_cert.pem
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDwzCCAqugAwIBAgIUXqdUxUOo8kmsDe71iTR+Vr7btP8wDQYJKoZIhvcNAQEL
|
||||
BQAwYjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
|
||||
EjAQBgNVBAoMCU5hdmlkcm9tZTENMAsGA1UECwwEVGVzdDESMBAGA1UEAwwJbG9j
|
||||
YWxob3N0MCAXDTI1MTEyODE5NTkxNVoYDzIxMjUxMTA0MTk1OTE1WjBiMQswCQYD
|
||||
VQQGEwJVUzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDESMBAGA1UECgwJ
|
||||
TmF2aWRyb21lMQ0wCwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkB/TQgl5ei5KRSHt5OJim8rKS
|
||||
MzRlkK4BjSEM4D9ESbebdpEVjX48QuBYACrCvgvVp7mQGF5anl8Hm89trvd8ooVQ
|
||||
x9IPQQ6gRKM+4gLrt9FHvFGGzZQS8UTQXN5oBi11E+8/Vs47HLUNXC2TRtRLCMyK
|
||||
LYXQIXbhdp9anImlt+IHUxIQUchK6Zkld/gCm56X1bbzN/Zq91PQLpx2FZ0eZTjN
|
||||
KaNgztLa+K/BDnTuk3iTTs9GEp6VCvqQE/6fk/UN/tkk2dLwKIFvPVR/YeAhVdz/
|
||||
OHC4L3B36QN3+VQ2yDjsp1PVAPX07UnzXO3Oj7uGYnMQxwprGMEubm3nADDxAgMB
|
||||
AAGjbzBtMB0GA1UdDgQWBBRAZHUVuLyzc0CfuZR9ApqMbawIqzAfBgNVHSMEGDAW
|
||||
gBRAZHUVuLyzc0CfuZR9ApqMbawIqzAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQT
|
||||
MBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAmDLXcPx9LNHs
|
||||
GxQIE6Q5BXbVO7c8qrWmJf5FK5VWaifNZ9U+IBi+VlB4jCLK/OkwsviN/jOnwRYx
|
||||
owjq0QG0YdRT4uD9fEMrAj+EwbnrQYZQvT0yGEWA+KW5TW08wt+/qnGJDwEgbjYJ
|
||||
HTdICVMhs/e8Ex48fAgO8WSsdTDekOrhuwzIfeJ1LU4ZptLsD2ePFxuzutdIuW51
|
||||
/mspQGsjXqZ1qnLsavLXh/lds2g602rTpYBNZVjV9WiOvaQS8vviOxBN6f+9vgRz
|
||||
a8SEbHqBG6jeyVqVZ7MjxcYxaIkxeBwMyMwgb+wwDfVXo2FZzX2TVeB7ZppI+IKv
|
||||
TXYurWPYsQ==
|
||||
-----END CERTIFICATE-----
|
||||
22
server/testdata/test_cert_encrypted.pem
vendored
Normal file
22
server/testdata/test_cert_encrypted.pem
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDpzCCAo+gAwIBAgIUEa7gEJYwJqYEJjTY7otQ+oUyELwwDQYJKoZIhvcNAQEL
|
||||
BQAwYjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
|
||||
EjAQBgNVBAoMCU5hdmlkcm9tZTENMAsGA1UECwwEVGVzdDESMBAGA1UEAwwJbG9j
|
||||
YWxob3N0MCAXDTI1MTEyODE5NTI0OVoYDzIxMjUxMTA0MTk1MjQ5WjBiMQswCQYD
|
||||
VQQGEwJVUzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDESMBAGA1UECgwJ
|
||||
TmF2aWRyb21lMQ0wCwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBHgqJ1d9EnNxqoSZ6xXrIz/mV
|
||||
Y0nWJW16/qIAvCdovSeTZhG9iqG8dUqcuu2BdD9MMHndJ2oFn3iD8EJR92dH8KBA
|
||||
8xOmtZ0BEEWgXPBivywZVd1ChIflEWj6m5wwLNjb57SPpUiwaLxBQB8ByEaAAZE/
|
||||
bLqvHI3vW/4s5apky17SPIqmkmqEYlRcg97tlRXsPuwoAVM9cvLMMEqtIR1CB/72
|
||||
gboY2Gi2r/plLF/Rg3Dom6QljMWi57XXWJFwGYSXaZuM0gvn04e3oLu+1E+WMoq/
|
||||
9rExWij2DlsmXd/RiScliFp6R4H84wQUyqrAUNytvgRO+oVnRjEA0l3oCYdRAgMB
|
||||
AAGjUzBRMB0GA1UdDgQWBBQQKpB1UaKm98FnBdl8uKdRscrVTzAfBgNVHSMEGDAW
|
||||
gBQQKpB1UaKm98FnBdl8uKdRscrVTzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQBP07l+2LmpFtcxqMGmsiNYwFuHpQCxJd4YRZHjLX7O+oJExMgR
|
||||
2yP4mpMKurgKOv7unTDLwvjQRa6ZTYJCsYtvC6hbyqlGc7AfNTu6DKz8r35/2/V5
|
||||
hPsG5lNb91HhvHE839mLAvpi02LoFH2Sr8BR7s6qxfNKYcP8PUOJQXltJ6yAa8YJ
|
||||
syeXQQ3RIyGsJANeaC06S3UdkBM5H5BLfIHnHu3GybJjwL51va4WCdHe8QV6GI0g
|
||||
RDiThDVkBSXAr136vnMdlrYCxMoxY56itJ0zbYg2ELQKU9o1w/ZJQo9uvmy9jCoZ
|
||||
Hy1L5a2vUDbsdONdvRkYZRHqMpG4bdD8D3j2
|
||||
-----END CERTIFICATE-----
|
||||
28
server/testdata/test_key.pem
vendored
Normal file
28
server/testdata/test_key.pem
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkB/TQgl5ei5KR
|
||||
SHt5OJim8rKSMzRlkK4BjSEM4D9ESbebdpEVjX48QuBYACrCvgvVp7mQGF5anl8H
|
||||
m89trvd8ooVQx9IPQQ6gRKM+4gLrt9FHvFGGzZQS8UTQXN5oBi11E+8/Vs47HLUN
|
||||
XC2TRtRLCMyKLYXQIXbhdp9anImlt+IHUxIQUchK6Zkld/gCm56X1bbzN/Zq91PQ
|
||||
Lpx2FZ0eZTjNKaNgztLa+K/BDnTuk3iTTs9GEp6VCvqQE/6fk/UN/tkk2dLwKIFv
|
||||
PVR/YeAhVdz/OHC4L3B36QN3+VQ2yDjsp1PVAPX07UnzXO3Oj7uGYnMQxwprGMEu
|
||||
bm3nADDxAgMBAAECggEABqJFvesP2v4FEvgd+kSWM+ZL34rPmy3zQ5/MDuPA20ep
|
||||
89EjQ/5hdRl1TknPcOnTu7PZVuENa9fM2xdrl7GEU9eU0bQLJE/KwiOUgJYObS8V
|
||||
eTO+DlghHXUBhfXDjux1CS+htOuTUqOyFNS+CR9Lta8o6ou1xjmcP7kW78i17mxF
|
||||
TuH5SZlS8W9PFLXHCInbMtqGFaT2ss09kvoPk2FDvHfxEdy6M9tKkguz02g+4bqI
|
||||
aAMp2N7AOfmRpC0HvVa1ZfZo5Z8/KMoNcIm3pV9DEVM369J9EzhnMNpkGben90aT
|
||||
FqO2JNsy52wmXFZUc9xe8uPdfDahALCkBGncLyLNmQKBgQDZREjocjdzOoPSlCdx
|
||||
mRNe9suHz2FpUpsHCPOCotG63hFVKpah/ZvpHSsQx5rXs/mawDTmzGY9GQiBrSvg
|
||||
OhfHIyT3NOhVaNcMxTqJX7rs7OG8D0MBacD9ASSeZ89MUn8q1EHZr5qxLtXl5Ikw
|
||||
mHtiGRdiKGFFrG9H0zncbGhy7QKBgQDBRhQ9RAasTdmUiNQly9GVFkXto4T/9UHx
|
||||
rVU44htCI2IVZUMTGlNfclfxpByDrzyA56rMzN9SAkiIp4nPpMDs5hayXaaPoojs
|
||||
CPzV7r2OjemZ6CTeQ1ODImRL8L/E3jJSgWd6YYoHSQ5hjEX4yT6ft0u0tZUfdMKd
|
||||
VENWIJ/hlQKBgQCo2hXjeOi5R8+tN3EUKwhP9HOnX7dv+D/9jqpZa5qdpPpJeyjI
|
||||
SmYCHKYci1Q+sWOaLiiu+km20B65UVFZGSzjmd+fs+GghzMifKGKo/iNK2ggFKhZ
|
||||
j8vplRrVdQ45XZ/xNDbdLEmHzEN2QE+Skd7KFYADzCgU0vdFFdbRBPuD3QKBgGIq
|
||||
fQctMRJ9LCE0akSURGwr9vKflmMHKCpfdqTAu0WZgS0K1Mm0GlqlUiPKzizYaauz
|
||||
f14sRNV7kWnPZsDPlqn8p9SKmpnj3RW97uWeMCtiyx6/+VHm8ljts/GaY1zT2s1r
|
||||
KqrPNfNDWQmU3MljNeqbh9lOTWK/xEVy0gzB31MNAoGAQNWrZvVdAbL95XW6STUu
|
||||
JmQlqJTlluuqS0Rrd/uVEQwW0Vd1dZjRQcFAFiSiCQWTbtId5gFZd6hiIQl53Xz0
|
||||
5cd+9mcyA/TaoCJYbMOFYsKbZMCBhefsovJlVQXedqJrIY6BdeGlet4GTAH5Qyl0
|
||||
ytEIUnvn5YmmbI7PDz80XpU=
|
||||
-----END PRIVATE KEY-----
|
||||
30
server/testdata/test_key_encrypted.pem
vendored
Normal file
30
server/testdata/test_key_encrypted.pem
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
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
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
30
server/testdata/test_key_encrypted_legacy.pem
vendored
Normal file
30
server/testdata/test_key_encrypted_legacy.pem
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,3C969050EAB73F121B7F0E6B75C42525
|
||||
|
||||
V6pSaAsrn9CQNo4p88QshJLbg8zkQJEom81dPbYSVqQSZa9YlPtpLZ9YtuLj/Ay0
|
||||
TScEKIj/gzQ32wNl6nhcSNIL9yy+X11r5gNv1kIHkecf+EbDW20VOiJsfD+6LUyW
|
||||
hA96AIbPOwc76iCuvsKHPKU9MlEmjGipmk/C2RQLHCZJ3WkiDRgCM8KQ7vKhfACT
|
||||
w908yj4cB1e/P0JPq8t/3F7kPJ+6SVM1vMEffHl0otQR3rAyrK8QikwJ0K9qX62d
|
||||
cqchTVlEyyZBYovR8DrRRUDbsXS5j1ZmX3NQpvTSTFowr+33fMrY+4Oz8sdR4yx1
|
||||
CQc0A0sHHxSEIr2xu4KzczwOYVJN8PVdU0pgvFj9KEm66N6EY5CSFIBHyO/ycOt9
|
||||
U+wpkRjf3zS6ZaUU0NKdOcop4YX33i99/tZF2RNR1i7ETLYph+/LCf09286Bi3u/
|
||||
UCCuWedyECPdz0c6j0s27Fdfc/HEK90OEzeWh/fc+H2gJZhqJYK9V47HPTQNNMnB
|
||||
U1a6FsJlrKE3E6nfSnTLxrSx9m/XTV7HV+HkgX+q8VhN7Q2VHUqkPzE7ZOPYpZ+A
|
||||
dQzsm1TmEMxym6osYqFzQScXR1NZasrV2MTQ2J16dUgCdGAM2YMUD9JaoJR+u77M
|
||||
WAjYzDiRg84rLr/KbJPAwHbsfo2KpiapJGSBBEDhz4W1/LOrFhsjaqIMSy4yZDGm
|
||||
1KqXGHIlqmuHI7v4fD8vuzhj7GUujRx85HSZWakE/uc6s5WrhkSeVKYJWPfpsxTv
|
||||
dT3oLOGJ+nRzWxM3aFtuJghX0nIGdKxT4EAUNXz0/vLT3OP1QCZR+oELrriFzmtj
|
||||
+O30bGH2SAFZEQJ/uTQg6celoNh89IzH4DJkcn67hqpX6mUiU9CrIr/eR9C/en8Q
|
||||
smTbbC1C1pDUaCwR26Z+zgM90amh4yfOFKK2geO2Kj+TmwFHUvi6ZnSzMzCvty3t
|
||||
+wdIrUtf55Lw51JCpLGl70mg4b/zBj5hqBkU2YvAAnz/htjfH/wrD6ZAF1TCdlRO
|
||||
gyODrJjGRnLd/v0XLk0wp+RkAjBcSlRlkUvZY5BtugL7dIdwiNGGQPcOni9IVeG0
|
||||
6vDUEQnDOLYDj4d/JcckTLuHdrP+SW+0RQl2HK5+/w1hScGXN4O48gccu7yR/MN8
|
||||
DmpCg5rD/nq8sxJosmSt07GrN36KppYt8LCXQbSg3NG2Ad715caS2C+0Qtdm5MPD
|
||||
rM1UyTXQYSJXgUN9yZS/pmzlguCywnnvsBPU6j3ljZwcoD41QJ/1OU09/W6sIMQR
|
||||
IAiM35JHiLJiccFgxSE1qx5F1UZqX4P47jF0Wzi/sE/DYXg5qw2DoauqXNzqnumH
|
||||
71UDGK1V6wQIV7UCZDa0WUfFzu470XpuFb8VmMOuHSQxkZESc9cz8k/ueAuO438Q
|
||||
jnlkF1Ge2EEPuaK2zeaTj/lGyYA1AUfHRRgt/EMUQSBntmhlpnwVPYTVvYtHO2N5
|
||||
wp7/y39KirnlTl99i3XiOJ4WF4gIU2IaSlqMo4+e/A32h2JFi9QfNyfItXe6Fm1X
|
||||
d0j2XGHzwMfHEFKdWyrgtVZwc38/1d6xWYAhs02b2basV/0AQhFTaKf5Z268eBNJ
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@ -47,17 +47,15 @@ const stylesheet = `
|
||||
.react-jinke-music-player-main .music-player-panel,
|
||||
.react-jinke-music-player-mobile,
|
||||
.ril__outer{
|
||||
background-color: #1f1f1f;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #fff1;
|
||||
}
|
||||
.ril__toolbar{
|
||||
background-color: #1d1d1d
|
||||
}
|
||||
.ril__toolbarItem{
|
||||
font-size: 100%;
|
||||
color: #eee
|
||||
}
|
||||
.audio-lists-panel{
|
||||
.audio-lists-panel,
|
||||
.ril__toolbar{
|
||||
background-color: #1f1f1f;
|
||||
border: 1px solid #fff1;
|
||||
border-radius: 6px 6px 0 0;
|
||||
|
||||
@ -137,22 +137,19 @@ export default {
|
||||
albumName: {
|
||||
color: '#eee',
|
||||
},
|
||||
albumSubtitle: {
|
||||
color: '#ccc',
|
||||
},
|
||||
albumPlayButton: {
|
||||
color: '#ff4e6b !important',
|
||||
color: '#ff4e6b',
|
||||
},
|
||||
albumArtistName: {
|
||||
color: '#ff4e6b !important',
|
||||
color: '#ccc',
|
||||
},
|
||||
cover: {
|
||||
borderRadius: '10px !important',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
},
|
||||
NDLogin: {
|
||||
systemNameLink: {
|
||||
color: '#D60017',
|
||||
color: '#ff4e6b',
|
||||
},
|
||||
welcome: {
|
||||
color: '#eee',
|
||||
@ -161,6 +158,9 @@ export default {
|
||||
minWidth: 300,
|
||||
backgroundColor: '#1d1d1d',
|
||||
},
|
||||
icon: {
|
||||
filter: 'hue-rotate(115deg)',
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
elevation1: {
|
||||
@ -169,6 +169,9 @@ export default {
|
||||
root: {
|
||||
color: '#eee',
|
||||
},
|
||||
rounded: {
|
||||
borderRadius: '6px',
|
||||
},
|
||||
},
|
||||
NDMobileArtistDetails: {
|
||||
bgContainer: {
|
||||
@ -189,6 +192,30 @@ export default {
|
||||
paddingBottom: '1rem',
|
||||
},
|
||||
},
|
||||
RaDeleteWithConfirmButton: {
|
||||
deleteButton: {
|
||||
color: 'unset',
|
||||
},
|
||||
},
|
||||
RaPaginationActions: {
|
||||
currentPageButton: {
|
||||
border: '2px solid #D60017',
|
||||
background: 'transparent',
|
||||
},
|
||||
button: {
|
||||
border: '2px solid #D60017',
|
||||
},
|
||||
actions: {
|
||||
'@global': {
|
||||
'.next-page': {
|
||||
border: '0 none',
|
||||
},
|
||||
'.previous-page': {
|
||||
border: '0 none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
theme: 'dark',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user