feat: add config-driven super-admin bootstrap to Go backend

This commit is contained in:
zn-admin committed 2026-08-14 07:43:05 +08:00
1 parent 8affa4b25a
commit ff055c972d
5 files changed
+345

No files matched your search

+17
View File
@@ -50,6 +50,23 @@ restart; it is intended only for development and contract smoke tests. The
default demo identity is the same optional-auth super administrator used by the
current Next development flow.
## First super administrator
On the first PostgreSQL deployment, the first super administrator is created
from configuration at startup — no separate script or CLI step is needed. When
the three variables below are all present and the process runs against
PostgreSQL, startup creates the account exactly once and skips the bootstrap
when any super administrator (including a disabled one) already exists:
- `ZHINIAN_BOOTSTRAP_ADMIN_PHONE`
- `ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD` (minimum 8 characters)
- `ZHINIAN_BOOTSTRAP_ADMIN_NAME` (defaults to `平台超级管理员`)
A failed bootstrap (invalid phone, short password, database error) fails
process startup so a misconfigured bootstrap is visible instead of silently
missing. Local development mode keeps the seeded demo administrator and never
bootstrap-creates accounts.
No Ingress, Docker, ACK, Secret, or Worker ownership has moved to Go yet, so
Next.js remains the deployed owner of every route. Real RDS/CA, OSS, provider,
Webhook, Worker drain/recovery, and rollback validation are mandatory before
@@ -3,6 +3,8 @@ package application
import (
"context"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
@@ -176,6 +178,11 @@ func New(options Options) (*App, error) {
})
administrationService := administration.NewService(administrationStore)
if created, err := BootstrapSuperAdmin(ctx, config.Backend, administrationService, ParseBootstrapAdminConfig(getenv)); err != nil {
return nil, fmt.Errorf("bootstrap super administrator: %w", err)
} else if created {
log.Printf("zhinian-api bootstrapped the first super administrator from ZHINIAN_BOOTSTRAP_ADMIN_* configuration")
}
adminHandler, err := httpapi.NewAdminHandler(platformAuthorizer, administrationService)
if err != nil {
return nil, err
@@ -0,0 +1,67 @@
package application
import (
"context"
"strings"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/administration"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/postgres"
)
// BootstrapAdminConfig describes the config-driven first super administrator.
// When every field is present and the process runs against PostgreSQL,
// application startup creates this account exactly once and skips the
// bootstrap when a super administrator already exists.
type BootstrapAdminConfig struct {
Phone string
Password string
DisplayName string
}
// Configured reports whether every bootstrap field is present.
func (c BootstrapAdminConfig) Configured() bool {
return c.Phone != "" && c.Password != "" && c.DisplayName != ""
}
// ParseBootstrapAdminConfig reads ZHINIAN_BOOTSTRAP_ADMIN_PHONE,
// ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD, and ZHINIAN_BOOTSTRAP_ADMIN_NAME. The
// variable names mirror the previous scripts/bootstrap-admin.mjs contract;
// the display name defaults to 平台超级管理员.
func ParseBootstrapAdminConfig(getenv func(string) string) BootstrapAdminConfig {
if getenv == nil {
return BootstrapAdminConfig{}
}
return BootstrapAdminConfig{
Phone: strings.TrimSpace(getenv("ZHINIAN_BOOTSTRAP_ADMIN_PHONE")),
Password: getenv("ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD"),
DisplayName: firstNonEmpty(strings.TrimSpace(getenv("ZHINIAN_BOOTSTRAP_ADMIN_NAME")), "平台超级管理员"),
}
}
// BootstrapSuperAdmin creates the configured first super administrator once.
// It is a no-op when the backend is not PostgreSQL, when the configuration is
// incomplete, or when a super administrator already exists (including
// disabled ones, so a retired administrator cannot trigger a duplicate).
// Failures are returned to the caller and fail application startup, so a
// misconfigured bootstrap is visible instead of silently missing.
func BootstrapSuperAdmin(ctx context.Context, backend postgres.Backend, service *administration.Service, config BootstrapAdminConfig) (bool, error) {
if backend != postgres.BackendPostgres || !config.Configured() {
return false, nil
}
existing, err := service.ListAccounts(ctx, administration.Actor{Role: administration.RoleSuperAdmin}, administration.AccountFilters{Role: administration.RoleSuperAdmin, IncludeDisabled: true})
if err != nil {
return false, err
}
if len(existing) > 0 {
return false, nil
}
if _, err := service.CreateAccount(ctx, administration.Actor{Role: administration.RoleSuperAdmin}, administration.CreateAccountInput{
Phone: config.Phone,
DisplayName: config.DisplayName,
Password: config.Password,
Role: administration.RoleSuperAdmin,
}); err != nil {
return false, err
}
return true, nil
}
@@ -0,0 +1,206 @@
package application
import (
"context"
"errors"
"testing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/administration"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/postgres"
)
// bootstrapFakeStore is a minimal in-memory administration.Store for
// bootstrap tests. Only ListAccounts and CreateAccount carry behavior; the
// remaining methods exist to satisfy the interface.
type bootstrapFakeStore struct {
accounts map[string]administration.Account
}
func newBootstrapFakeStore() *bootstrapFakeStore {
return &bootstrapFakeStore{accounts: map[string]administration.Account{}}
}
func (s *bootstrapFakeStore) ListAccounts(_ context.Context, filters administration.AccountFilters) ([]administration.Account, error) {
out := []administration.Account{}
for _, account := range s.accounts {
if filters.OrganizationID != "" && account.OrganizationID != filters.OrganizationID {
continue
}
if filters.Role != "" && account.Role != filters.Role {
continue
}
if !filters.IncludeDisabled && account.Status != administration.StatusActive {
continue
}
out = append(out, account)
}
return out, nil
}
func (s *bootstrapFakeStore) GetAccount(_ context.Context, id string) (administration.Account, bool, error) {
account, ok := s.accounts[id]
return account, ok, nil
}
func (s *bootstrapFakeStore) CreateAccount(_ context.Context, account administration.Account) (administration.Account, error) {
if _, exists := s.accounts[account.ID]; exists {
return administration.Account{}, errors.New("account already exists")
}
s.accounts[account.ID] = account
return account, nil
}
func (s *bootstrapFakeStore) UpdateAccount(_ context.Context, account administration.Account) (administration.Account, error) {
s.accounts[account.ID] = account
return account, nil
}
func (s *bootstrapFakeStore) DeleteAccount(_ context.Context, id, _ string) error {
delete(s.accounts, id)
return nil
}
func (s *bootstrapFakeStore) ListOrganizations(context.Context, bool) ([]administration.Organization, error) {
return nil, nil
}
func (s *bootstrapFakeStore) GetOrganization(context.Context, string) (administration.Organization, bool, error) {
return administration.Organization{}, false, nil
}
func (s *bootstrapFakeStore) CreateOrganization(_ context.Context, organization administration.Organization) (administration.Organization, error) {
return organization, nil
}
func (s *bootstrapFakeStore) UpdateOrganization(_ context.Context, organization administration.Organization) (administration.Organization, error) {
return organization, nil
}
func (s *bootstrapFakeStore) DeleteOrganization(context.Context, string) error { return nil }
func (s *bootstrapFakeStore) CountOrganizationMembers(context.Context, string) (int, error) {
return 0, nil
}
func configuredBootstrap() BootstrapAdminConfig {
return BootstrapAdminConfig{Phone: "13800138000", Password: "change-me-now", DisplayName: "平台超级管理员"}
}
func TestParseBootstrapAdminConfig(t *testing.T) {
env := map[string]string{
"ZHINIAN_BOOTSTRAP_ADMIN_PHONE": " 13800138000 ",
"ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD": "change-me-now",
"ZHINIAN_BOOTSTRAP_ADMIN_NAME": " 平台超级管理员 ",
}
config := ParseBootstrapAdminConfig(func(name string) string { return env[name] })
if config.Phone != "13800138000" || config.Password != "change-me-now" || config.DisplayName != "平台超级管理员" {
t.Fatalf("unexpected parsed config: %+v", config)
}
if !config.Configured() {
t.Fatalf("expected parsed config to be complete")
}
defaulted := ParseBootstrapAdminConfig(func(name string) string {
if name == "ZHINIAN_BOOTSTRAP_ADMIN_NAME" {
return ""
}
return env[name]
})
if defaulted.DisplayName != "平台超级管理员" {
t.Fatalf("expected default display name, got %q", defaulted.DisplayName)
}
if config := ParseBootstrapAdminConfig(nil); config.Configured() {
t.Fatalf("nil getenv must produce an incomplete config")
}
}
func TestBootstrapSuperAdminNoopWithoutConfig(t *testing.T) {
store := newBootstrapFakeStore()
service := administration.NewService(store)
created, err := BootstrapSuperAdmin(context.Background(), postgres.BackendPostgres, service, BootstrapAdminConfig{})
if err != nil || created {
t.Fatalf("expected no-op without config, got created=%v err=%v", created, err)
}
if len(store.accounts) != 0 {
t.Fatalf("expected no accounts, got %d", len(store.accounts))
}
}
func TestBootstrapSuperAdminNoopOnLocalBackend(t *testing.T) {
store := newBootstrapFakeStore()
service := administration.NewService(store)
created, err := BootstrapSuperAdmin(context.Background(), postgres.BackendLocal, service, configuredBootstrap())
if err != nil || created {
t.Fatalf("expected no-op on local backend, got created=%v err=%v", created, err)
}
if len(store.accounts) != 0 {
t.Fatalf("expected no accounts, got %d", len(store.accounts))
}
}
func TestBootstrapSuperAdminCreatesOnce(t *testing.T) {
store := newBootstrapFakeStore()
service := administration.NewService(store)
created, err := BootstrapSuperAdmin(context.Background(), postgres.BackendPostgres, service, configuredBootstrap())
if err != nil || !created {
t.Fatalf("expected first bootstrap to create, got created=%v err=%v", created, err)
}
if len(store.accounts) != 1 {
t.Fatalf("expected exactly one account, got %d", len(store.accounts))
}
for _, account := range store.accounts {
if account.Role != administration.RoleSuperAdmin || account.Status != administration.StatusActive {
t.Fatalf("unexpected bootstrapped account: %+v", account)
}
if account.PasswordHash == "change-me-now" || account.PasswordHash == "" {
t.Fatalf("password must be hashed, got %q", account.PasswordHash)
}
}
created, err = BootstrapSuperAdmin(context.Background(), postgres.BackendPostgres, service, configuredBootstrap())
if err != nil || created {
t.Fatalf("expected second bootstrap to skip, got created=%v err=%v", created, err)
}
if len(store.accounts) != 1 {
t.Fatalf("expected still exactly one account, got %d", len(store.accounts))
}
}
func TestBootstrapSuperAdminSkipsDisabledSuperAdmin(t *testing.T) {
store := newBootstrapFakeStore()
existing := administration.Account{
ID: "user-existing", Phone: "13900139000", DisplayName: "已有超管",
Role: administration.RoleSuperAdmin, Status: administration.StatusDisabled,
SessionVersion: 1,
}
store.accounts[existing.ID] = existing
service := administration.NewService(store)
created, err := BootstrapSuperAdmin(context.Background(), postgres.BackendPostgres, service, configuredBootstrap())
if err != nil || created {
t.Fatalf("expected skip with existing disabled super admin, got created=%v err=%v", created, err)
}
if len(store.accounts) != 1 {
t.Fatalf("expected no new account, got %d", len(store.accounts))
}
}
func TestBootstrapSuperAdminRejectsShortPassword(t *testing.T) {
store := newBootstrapFakeStore()
service := administration.NewService(store)
config := configuredBootstrap()
config.Password = "short"
created, err := BootstrapSuperAdmin(context.Background(), postgres.BackendPostgres, service, config)
if err == nil {
t.Fatalf("expected short password to fail")
}
if created {
t.Fatalf("expected no account on failed bootstrap")
}
if len(store.accounts) != 0 {
t.Fatalf("expected no accounts, got %d", len(store.accounts))
}
}