From e6b84190e7842b854e40722233361ba7f21f7c13 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 19 Apr 2026 18:12:13 -0400 Subject: [PATCH 1/2] chore(compose): bake in dev mount + scan defaults Dev stack can now `docker compose up --build` with no extra env: mounts ./music read-only, enables startup scan, and bootstraps admin with a generated password printed to stderr on first boot. Co-Authored-By: Claude Opus 4.7 --- docker-compose.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 1ea00da2..88619106 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,8 +34,18 @@ services: environment: SMARTMUSIC_DATABASE_URL: postgres://minstrel:minstrel@postgres:5432/minstrel?sslmode=disable SMARTMUSIC_LOG_FORMAT: text + SMARTMUSIC_LIBRARY_SCAN_PATHS: /music + SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP: "true" + SMARTMUSIC_AUTH_ADMIN_USERNAME: admin + # SMARTMUSIC_AUTH_ADMIN_PASSWORD left unset on purpose — bootstrap + # generates one and prints it to stderr on first boot. Watch the logs + # of the minstrel service the first time you bring the stack up. ports: - "4533:4533" + volumes: + # Point ./music at your test library (symlink, bind-mount, whatever). + # Read-only so a buggy scanner can't rewrite your files. + - ./music:/music:ro volumes: minstrel-pgdata: From 64582b21e316d090876be29fc5125f2bfe96d2ec Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 19 Apr 2026 18:30:54 -0400 Subject: [PATCH 2/2] fix(auth): set subsonic_password on admin bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bootstrap was creating password_hash + api_token but leaving subsonic_password nil, which meant Subsonic clients (Feishin, Symfonium) got ErrTokenNotSupported on t+s auth — the server had no plaintext to hash against. Mirror the bootstrap password into subsonic_password so the admin can sign in to Subsonic clients with the same credential printed on first boot. Plaintext at rest is the cost of Subsonic's legacy auth; matches Navidrome's posture. Also folds in the local dev compose tweaks: dedicated bridge network with postgres unpublished from the host, and a bind-mount aimed at the operator's real library path. Co-Authored-By: Claude Opus 4.7 --- docker-compose.yml | 13 ++++++++++--- internal/auth/bootstrap.go | 18 ++++++++++++++++-- internal/auth/bootstrap_test.go | 3 +++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 88619106..997de037 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,10 @@ services: POSTGRES_USER: minstrel POSTGRES_PASSWORD: minstrel POSTGRES_DB: minstrel - ports: - - "5432:5432" + networks: + - minstrel + # ports: + # - "5432:5432" volumes: - minstrel-pgdata:/var/lib/postgresql/data healthcheck: @@ -40,12 +42,17 @@ services: # SMARTMUSIC_AUTH_ADMIN_PASSWORD left unset on purpose — bootstrap # generates one and prints it to stderr on first boot. Watch the logs # of the minstrel service the first time you bring the stack up. + networks: + - minstrel ports: - "4533:4533" volumes: # Point ./music at your test library (symlink, bind-mount, whatever). # Read-only so a buggy scanner can't rewrite your files. - - ./music:/music:ro + - /mnt/Media/Music:/music:ro volumes: minstrel-pgdata: + +networks: + minstrel: \ No newline at end of file diff --git a/internal/auth/bootstrap.go b/internal/auth/bootstrap.go index 00c5b02e..ad7b2826 100644 --- a/internal/auth/bootstrap.go +++ b/internal/auth/bootstrap.go @@ -64,12 +64,26 @@ func Bootstrap(ctx context.Context, pool *pgxpool.Pool, cfg config.AdminBootstra return fmt.Errorf("auth: insert admin: %w", err) } + // Mirror the bootstrap password into subsonic_password so Subsonic clients + // (Feishin, Symfonium, …) can authenticate via t+s without the operator + // having to run a separate SQL update after first boot. This is plaintext + // at rest — the cost of Subsonic's legacy auth — and matches what servers + // like Navidrome do. Operators who don't want a plaintext copy can clear + // it later with SetSubsonicPassword(nil). + subPass := password + if err := q.SetSubsonicPassword(ctx, dbq.SetSubsonicPasswordParams{ + ID: user.ID, + SubsonicPassword: &subPass, + }); err != nil { + return fmt.Errorf("auth: set subsonic password: %w", err) + } + logger.Info("admin bootstrapped", "username", cfg.Username, "user_id", user.ID.String()) if passwordGenerated { - fmt.Fprintf(os.Stderr, "\n--- minstrel admin bootstrap (shown once) ---\nusername: %s\npassword: %s\napi_token: %s\n---\n\n", + fmt.Fprintf(os.Stderr, "\n--- minstrel admin bootstrap (shown once) ---\nusername: %s\npassword: %s\napi_token: %s\n(password works for both the native UI and Subsonic clients)\n---\n\n", cfg.Username, password, apiToken) } else { - fmt.Fprintf(os.Stderr, "\n--- minstrel admin api token (shown once) ---\nusername: %s\napi_token: %s\n---\n\n", + fmt.Fprintf(os.Stderr, "\n--- minstrel admin api token (shown once) ---\nusername: %s\napi_token: %s\n(your configured password now also works for Subsonic clients)\n---\n\n", cfg.Username, apiToken) } return nil diff --git a/internal/auth/bootstrap_test.go b/internal/auth/bootstrap_test.go index 7fe6ec5b..6bb7e8ce 100644 --- a/internal/auth/bootstrap_test.go +++ b/internal/auth/bootstrap_test.go @@ -63,6 +63,9 @@ func TestBootstrap_Integration(t *testing.T) { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte("hunter2")); err != nil { t.Errorf("bcrypt compare: %v", err) } + if user.SubsonicPassword == nil || *user.SubsonicPassword != "hunter2" { + t.Errorf("subsonic_password = %v, want \"hunter2\"", user.SubsonicPassword) + } byToken, err := q.GetUserByAPIToken(ctx, user.ApiToken) if err != nil {