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 {