refactor(server): remove bootstrap admin path

The bootstrap-admin-from-env-vars flow was a holdover from before
self-registration could create the first admin. Now that handleRegister
+ CreateUserFirstAdminRace promotes the first user to register on an
empty users table (verified shipped in v2026.05.08.2 / #376), the
bootstrap path is just a second source of truth that confuses operators
(and leaves an "admin" account in the DB that nobody asked for).

Removes:
- internal/auth/bootstrap.go + its test
- The auth.Bootstrap call from cmd/minstrel/main.go
- AuthConfig + AdminBootstrapConfig structs from config
- MINSTREL_AUTH_ADMIN_USERNAME / _PASSWORD env reads + their test
- The auth: block from config.example.yaml
- Bootstrap-related comments in docker-compose.yml + README.md

The README quickstart now points operators at /register on first start
instead of "watch the logs for a one-time password."

Existing instances keep their bootstrap-admin row in the DB; operators
who want it gone can register a new admin via /register, promote them
in /admin/users, then delete the old bootstrap user (last-admin guard
will require the new admin to be promoted first). No migration needed.

Recovery story for forgotten admin passwords now hinges on Fable #321
(admin password reset CLI) — currently the only path back in if no
other admin exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 22:14:33 -04:00
parent d5c8d316c5
commit c4cccea775
8 changed files with 1 additions and 303 deletions
-24
View File
@@ -14,7 +14,6 @@ type Config struct {
Storage StorageConfig `yaml:"storage"`
Database DatabaseConfig `yaml:"database"`
Log LogConfig `yaml:"log"`
Auth AuthConfig `yaml:"auth"`
Library LibraryConfig `yaml:"library"`
Subsonic SubsonicConfig `yaml:"subsonic"`
Events EventsConfig `yaml:"events"`
@@ -56,18 +55,6 @@ type LogConfig struct {
Format string `yaml:"format"`
}
type AuthConfig struct {
AdminBootstrap AdminBootstrapConfig `yaml:"admin_bootstrap"`
}
// AdminBootstrapConfig drives the one-shot admin creation on an empty users
// table. Username is required to enable the flow; leaving Password blank asks
// the server to generate a random one and log it to stderr.
type AdminBootstrapConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type LibraryConfig struct {
ScanPaths []string `yaml:"scan_paths"`
ScanOnStartup bool `yaml:"scan_on_startup"`
@@ -115,11 +102,6 @@ func Default() Config {
AppName: "Minstrel",
Description: "Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration.",
},
Auth: AuthConfig{
AdminBootstrap: AdminBootstrapConfig{
Username: "admin",
},
},
Events: EventsConfig{
SessionTimeoutMinutes: 30,
SkipMaxCompletionRatio: 0.5,
@@ -182,12 +164,6 @@ func applyEnv(cfg *Config) {
if v, ok := os.LookupEnv("MINSTREL_BRANDING_DESCRIPTION"); ok && v != "" {
cfg.Branding.Description = v
}
if v, ok := os.LookupEnv("MINSTREL_AUTH_ADMIN_USERNAME"); ok {
cfg.Auth.AdminBootstrap.Username = v
}
if v, ok := os.LookupEnv("MINSTREL_AUTH_ADMIN_PASSWORD"); ok {
cfg.Auth.AdminBootstrap.Password = v
}
if v, ok := os.LookupEnv("MINSTREL_LIBRARY_SCAN_PATHS"); ok {
cfg.Library.ScanPaths = splitPaths(v)
}
-16
View File
@@ -79,22 +79,6 @@ func TestEnvOverrides(t *testing.T) {
}
}
func TestAdminBootstrapEnvOverrides(t *testing.T) {
t.Setenv("MINSTREL_AUTH_ADMIN_USERNAME", "root")
t.Setenv("MINSTREL_AUTH_ADMIN_PASSWORD", "sekret")
cfg, err := Load("")
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Auth.AdminBootstrap.Username != "root" {
t.Errorf("admin username = %q", cfg.Auth.AdminBootstrap.Username)
}
if cfg.Auth.AdminBootstrap.Password != "sekret" {
t.Errorf("admin password = %q", cfg.Auth.AdminBootstrap.Password)
}
}
func TestLibraryEnvOverrides(t *testing.T) {
t.Setenv("MINSTREL_LIBRARY_SCAN_PATHS", "/music:/other::/third")
t.Setenv("MINSTREL_LIBRARY_SCAN_ON_STARTUP", "true")