fix(auth): sync bootstrap admin into user billing (#1932)

This commit is contained in:
Lyon
2026-06-23 00:42:23 +08:00
committed by GitHub
parent a09e3cc20e
commit d4275645e9
2 changed files with 109 additions and 28 deletions
+105 -28
View File
@@ -35,20 +35,26 @@ const serviceID = "hwlab-user-billing"
var migrationFS embed.FS
type Config struct {
Addr string
DatabaseURL string
RedisURL string
InitialCredits int64
CreditPerThousandTok int64
SessionTTL time.Duration
ReservationTTL time.Duration
RegistrationEnabled bool
InternalToken string
RequireInternalToken bool
MigrateOnStart bool
StateAuthority string
RuntimeNamespace string
ExternalDatabaseLabel string
Addr string
DatabaseURL string
RedisURL string
InitialCredits int64
CreditPerThousandTok int64
SessionTTL time.Duration
ReservationTTL time.Duration
RegistrationEnabled bool
InternalToken string
RequireInternalToken bool
MigrateOnStart bool
StateAuthority string
RuntimeNamespace string
ExternalDatabaseLabel string
BootstrapAdminID string
BootstrapAdminEmail string
BootstrapAdminUsername string
BootstrapAdminDisplayName string
BootstrapAdminPasswordHash string
BootstrapAdminPassword string
}
type Server struct {
@@ -315,20 +321,26 @@ func LoadConfig(env []string) Config {
}
port := first(values["HWLAB_USER_BILLING_PORT"], values["PORT"], "6670")
cfg := Config{
Addr: first(values["HWLAB_USER_BILLING_ADDR"], ":"+port),
DatabaseURL: first(values["HWLAB_USER_BILLING_DB_URL"], values["DATABASE_URL"]),
RedisURL: values["HWLAB_USER_BILLING_REDIS_URL"],
InitialCredits: parseInt64(values["HWLAB_USER_BILLING_INITIAL_CREDITS"], 0),
CreditPerThousandTok: parseInt64(values["HWLAB_USER_BILLING_CREDIT_PER_1K_TOKENS"], 1),
SessionTTL: parseDurationHours(values["HWLAB_USER_BILLING_SESSION_TTL_HOURS"], 720),
ReservationTTL: parseDurationMinutes(values["HWLAB_USER_BILLING_RESERVATION_TTL_MINUTES"], 30),
RegistrationEnabled: values["HWLAB_USER_BILLING_REGISTRATION_ENABLED"] != "0",
InternalToken: values["HWLAB_USER_BILLING_INTERNAL_TOKEN"],
RequireInternalToken: values["HWLAB_USER_BILLING_REQUIRE_INTERNAL_TOKEN"] == "1",
MigrateOnStart: values["HWLAB_USER_BILLING_MIGRATE_ON_START"] != "0",
StateAuthority: first(values["HWLAB_USER_BILLING_STATE_AUTHORITY"], "pk01-postgres"),
RuntimeNamespace: first(values["POD_NAMESPACE"], values["HWLAB_NAMESPACE"], "hwlab-v03"),
ExternalDatabaseLabel: first(values["HWLAB_USER_BILLING_EXTERNAL_DB_LABEL"], "pk01-external-postgres"),
Addr: first(values["HWLAB_USER_BILLING_ADDR"], ":"+port),
DatabaseURL: first(values["HWLAB_USER_BILLING_DB_URL"], values["DATABASE_URL"]),
RedisURL: values["HWLAB_USER_BILLING_REDIS_URL"],
InitialCredits: parseInt64(values["HWLAB_USER_BILLING_INITIAL_CREDITS"], 0),
CreditPerThousandTok: parseInt64(values["HWLAB_USER_BILLING_CREDIT_PER_1K_TOKENS"], 1),
SessionTTL: parseDurationHours(values["HWLAB_USER_BILLING_SESSION_TTL_HOURS"], 720),
ReservationTTL: parseDurationMinutes(values["HWLAB_USER_BILLING_RESERVATION_TTL_MINUTES"], 30),
RegistrationEnabled: values["HWLAB_USER_BILLING_REGISTRATION_ENABLED"] != "0",
InternalToken: values["HWLAB_USER_BILLING_INTERNAL_TOKEN"],
RequireInternalToken: values["HWLAB_USER_BILLING_REQUIRE_INTERNAL_TOKEN"] == "1",
MigrateOnStart: values["HWLAB_USER_BILLING_MIGRATE_ON_START"] != "0",
StateAuthority: first(values["HWLAB_USER_BILLING_STATE_AUTHORITY"], "pk01-postgres"),
RuntimeNamespace: first(values["POD_NAMESPACE"], values["HWLAB_NAMESPACE"], "hwlab-v03"),
ExternalDatabaseLabel: first(values["HWLAB_USER_BILLING_EXTERNAL_DB_LABEL"], "pk01-external-postgres"),
BootstrapAdminID: first(values["HWLAB_BOOTSTRAP_ADMIN_ID"], "usr_bootstrap_admin"),
BootstrapAdminEmail: values["HWLAB_BOOTSTRAP_ADMIN_EMAIL"],
BootstrapAdminUsername: first(values["HWLAB_BOOTSTRAP_ADMIN_USERNAME"], "admin"),
BootstrapAdminDisplayName: first(values["HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME"], "HWLAB Admin"),
BootstrapAdminPasswordHash: values["HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH"],
BootstrapAdminPassword: values["HWLAB_BOOTSTRAP_ADMIN_PASSWORD"],
}
return cfg
}
@@ -351,11 +363,67 @@ func NewServer(ctx context.Context, cfg Config) (*Server, error) {
return nil, fmt.Errorf("migrate user billing schema: %w", err)
}
}
bootstrapCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := server.ensureBootstrapAdmin(bootstrapCtx); err != nil {
return nil, fmt.Errorf("ensure bootstrap admin: %w", err)
}
}
server.routes()
return server, nil
}
func (s *Server) ensureBootstrapAdmin(ctx context.Context) error {
if s.db == nil {
return nil
}
username := normalizeUsername(s.config.BootstrapAdminUsername)
if username == "" {
return nil
}
passwordHash := strings.TrimSpace(s.config.BootstrapAdminPasswordHash)
if passwordHash == "" && strings.TrimSpace(s.config.BootstrapAdminPassword) != "" {
hash, err := hashPassword(s.config.BootstrapAdminPassword)
if err != nil {
return err
}
passwordHash = hash
}
if passwordHash == "" {
return nil
}
adminID := strings.TrimSpace(first(s.config.BootstrapAdminID, "usr_bootstrap_admin"))
email := strings.ToLower(strings.TrimSpace(s.config.BootstrapAdminEmail))
if email == "" {
email = username + "@bootstrap.hwlab.local"
}
displayName := strings.TrimSpace(first(s.config.BootstrapAdminDisplayName, "HWLAB Admin"))
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
defer tx.Rollback()
result, err := tx.ExecContext(ctx, `UPDATE hwlab_users SET email = $3, display_name = $4, password_hash = $5, status = 'active', role = 'admin', email_verified = true, updated_at = now() WHERE id = $1 OR username = $2`, adminID, username, email, displayName, passwordHash)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role, email_verified) VALUES ($1, $2, $3, $4, $5, 'active', 'admin', true)`, adminID, email, username, displayName, passwordHash)
if err != nil {
return err
}
}
_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_accounts (user_id, balance_credits, reserved_credits, plan_id) VALUES ($1, $2, 0, 'default') ON CONFLICT (user_id) DO NOTHING`, adminID, nonNegative(s.config.InitialCredits))
if err != nil {
return err
}
return tx.Commit()
}
func (s *Server) Close() {
if s.db != nil {
_ = s.db.Close()
@@ -2744,6 +2812,15 @@ func hashPassword(password string) (string, error) {
}
func verifyPassword(encoded, password string) bool {
if strings.HasPrefix(encoded, "sha256:") {
parts := strings.Split(encoded, ":")
if len(parts) != 3 || parts[1] == "" || parts[2] == "" {
return false
}
digest := sha256.Sum256([]byte(parts[1] + ":" + password))
actual := hex.EncodeToString(digest[:])
return subtle.ConstantTimeCompare([]byte(actual), []byte(parts[2])) == 1
}
parts := strings.Split(encoded, "$")
if len(parts) != 4 || parts[0] != "pbkdf2_sha256" {
return false