fix(backend): bootstrap admin from env instead of seeding admin/admin

007_seed_data.sql shipped a fixed admin account whose bcrypt hash decodes
to the password "admin", giving every deployment the same known
credentials. The seed row is removed; UserService.EnsureAdmin now creates
the administrator on startup from ADMIN_USERNAME / ADMIN_PASSWORD. It is
idempotent and never overwrites an existing password, so an operator who
rotates the admin password keeps it across restarts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:01:48 +03:00
parent 945df7ef8a
commit 9ea939ccf6
6 changed files with 56 additions and 3 deletions
+31
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
@@ -21,6 +22,36 @@ func NewUserService(users port.UserRepo, audit *AuditService) *UserService {
return &UserService{users: users, audit: audit}
}
// EnsureAdmin creates the initial administrator account if it does not already
// exist. It is idempotent and never overwrites an existing user's password, so
// an operator who has changed the admin password keeps it across restarts.
func (s *UserService) EnsureAdmin(ctx context.Context, username, password string) error {
if username == "" || password == "" {
return fmt.Errorf("EnsureAdmin: username and password must be set")
}
if _, err := s.users.GetByName(ctx, username); err == nil {
return nil // already exists
} else if !errors.Is(err, domain.ErrNotFound) {
return fmt.Errorf("EnsureAdmin: lookup: %w", err)
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("EnsureAdmin: hash: %w", err)
}
_, err = s.users.Create(ctx, &domain.User{
Name: username,
Password: string(hash),
IsAdmin: true,
CanCreate: true,
})
if err != nil {
return fmt.Errorf("EnsureAdmin: create: %w", err)
}
return nil
}
// ---------------------------------------------------------------------------
// Self-service
// ---------------------------------------------------------------------------