fix(auth): sync bootstrap admin into user billing (#1932)
This commit is contained in:
@@ -599,6 +599,10 @@ lanes:
|
||||
- serviceId: hwlab-user-billing
|
||||
env:
|
||||
HWLAB_USER_BILLING_DB_URL: secretRef:hwlab-cloud-api-v03-db/database-url
|
||||
HWLAB_BOOTSTRAP_ADMIN_ID: usr_v03_admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME: HWLAB v0.3 Admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH: secretRef:hwlab-v03-bootstrap-admin/password-hash
|
||||
HWLAB_USER_BILLING_STATE_AUTHORITY: pk01-postgres
|
||||
HWLAB_USER_BILLING_EXTERNAL_DB_LABEL: pk01-external-postgres
|
||||
HWLAB_USER_BILLING_PORT: "6670"
|
||||
|
||||
+105
-28
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user