ui优化
This commit is contained in:
1 parent
648b274c55
commit
18cb51670c
14 files changed
+420
-171
No files matched your search
@@ -22,6 +22,7 @@ import (
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/postgres"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/prompt"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/publicapi"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/settings"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/templates"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/usage"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/webhook"
|
||||
@@ -45,10 +46,12 @@ type Options struct {
|
||||
RemoteFetcher assets.RemoteFetcher
|
||||
// ProviderRegistry can replace all external adapters in deterministic tests.
|
||||
ProviderRegistry jobs.ProviderRegistry
|
||||
// Log adapters remain injectable for deterministic composition tests.
|
||||
// Runtime settings intentionally have one concrete source so /api/settings
|
||||
// and billing account endpoints cannot observe different stores.
|
||||
// Log and runtime-settings adapters remain injectable for deterministic
|
||||
// composition tests. Production uses the PostgreSQL Store opened below.
|
||||
Logs httpapi.LogService
|
||||
// Runtime settings intentionally have one source so /api/settings, startup
|
||||
// services, dynamic providers, and billing account endpoints stay coherent.
|
||||
RuntimeSettingsRepository settings.RuntimeSettingsRepository
|
||||
}
|
||||
|
||||
type App struct {
|
||||
@@ -71,10 +74,6 @@ func New(options Options) (*App, error) {
|
||||
readFile = os.ReadFile
|
||||
}
|
||||
|
||||
authConfig, err := ParseAuthConfig(getenv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := postgres.ParseConfig(getenv, readFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,6 +88,22 @@ func New(options Options) (*App, error) {
|
||||
database.Close()
|
||||
}
|
||||
}()
|
||||
runtimeSettings := defaultSettingsService(getenv)
|
||||
if config.Backend == postgres.BackendPostgres {
|
||||
repository := options.RuntimeSettingsRepository
|
||||
if repository == nil {
|
||||
repository = database.Store
|
||||
}
|
||||
runtimeSettings = databaseSettingsService(getenv, repository)
|
||||
}
|
||||
startupGetenv, err := runtimeSettingsGetenv(ctx, getenv, runtimeSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load startup runtime settings: %w", err)
|
||||
}
|
||||
authConfig, err := ParseAuthConfig(startupGetenv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readiness := databaseReadiness{config: config, store: database.Store}
|
||||
|
||||
// PostgreSQL remains the production source of truth. Local mode swaps every
|
||||
@@ -135,7 +150,7 @@ func New(options Options) (*App, error) {
|
||||
platformAuthorizer, err := httpapi.NewPlatformAuthorizer(
|
||||
authState,
|
||||
resolver,
|
||||
httpapi.WithLocalDevelopmentFallback(config.Backend == postgres.BackendLocal && !strings.EqualFold(strings.TrimSpace(getenv("NODE_ENV")), "production")),
|
||||
httpapi.WithLocalDevelopmentFallback(config.Backend == postgres.BackendLocal && !strings.EqualFold(strings.TrimSpace(startupGetenv("NODE_ENV")), "production")),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -154,8 +169,8 @@ func New(options Options) (*App, error) {
|
||||
}
|
||||
passwordIssuer = identity.NewPasswordLogin(authenticator, nil)
|
||||
}
|
||||
cookieSecure := getenv("ZHINIAN_AUTH_COOKIE_SECURE")
|
||||
publicBaseURL := firstAuthEnv(getenv, "NEXT_PUBLIC_APP_URL", "ZHINIAN_PUBLIC_BASE_URL")
|
||||
cookieSecure := startupGetenv("ZHINIAN_AUTH_COOKIE_SECURE")
|
||||
publicBaseURL := firstAuthEnv(startupGetenv, "NEXT_PUBLIC_APP_URL", "ZHINIAN_PUBLIC_BASE_URL")
|
||||
authPassword, err := httpapi.NewAuthPasswordHandler(httpapi.PasswordAuthConfig{
|
||||
Configured: authConfig.Configured,
|
||||
SessionSecret: authConfig.SessionSecret,
|
||||
@@ -172,13 +187,13 @@ func New(options Options) (*App, error) {
|
||||
authCompatibility := httpapi.NewAuthCompatibilityHandler()
|
||||
|
||||
publicAuthenticator := publicapi.NewAuthenticator(publicapi.Config{
|
||||
APIKeys: getenv("ZHINIAN_API_KEYS"),
|
||||
InternalWorkerToken: getenv("ZHINIAN_INTERNAL_WORKER_TOKEN"),
|
||||
Production: strings.EqualFold(strings.TrimSpace(getenv("NODE_ENV")), "production"),
|
||||
APIKeys: startupGetenv("ZHINIAN_API_KEYS"),
|
||||
InternalWorkerToken: startupGetenv("ZHINIAN_INTERNAL_WORKER_TOKEN"),
|
||||
Production: strings.EqualFold(strings.TrimSpace(startupGetenv("NODE_ENV")), "production"),
|
||||
})
|
||||
|
||||
administrationService := administration.NewService(administrationStore)
|
||||
if created, err := BootstrapSuperAdmin(ctx, config.Backend, administrationService, ParseBootstrapAdminConfig(getenv)); err != nil {
|
||||
if created, err := BootstrapSuperAdmin(ctx, config.Backend, administrationService, ParseBootstrapAdminConfig(startupGetenv)); 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")
|
||||
@@ -201,12 +216,12 @@ func New(options Options) (*App, error) {
|
||||
blobStore := options.BlobStore
|
||||
if blobStore == nil {
|
||||
var configured bool
|
||||
blobStore, configured, err = configuredOSSBlobStore(getenv)
|
||||
blobStore, configured, err = configuredOSSBlobStore(startupGetenv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !configured {
|
||||
runtimeDirectory := strings.TrimSpace(getenv("ZHINIAN_RUNTIME_DIR"))
|
||||
runtimeDirectory := strings.TrimSpace(startupGetenv("ZHINIAN_RUNTIME_DIR"))
|
||||
if runtimeDirectory == "" {
|
||||
runtimeDirectory = filepath.Join(".runtime")
|
||||
}
|
||||
@@ -220,7 +235,7 @@ func New(options Options) (*App, error) {
|
||||
if remoteFetcher == nil {
|
||||
remoteFetcher, err = assets.NewPublicHTTPRemoteFetcher(
|
||||
30*time.Second,
|
||||
remoteAssetMaxBytes(getenv),
|
||||
remoteAssetMaxBytes(startupGetenv),
|
||||
assets.NewPublicDestinationPolicy(nil, nil),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -229,17 +244,13 @@ func New(options Options) (*App, error) {
|
||||
}
|
||||
assetService := assets.NewService(assetCatalog, blobStore, remoteFetcher, nil, nil)
|
||||
assetsHandler, err := httpapi.NewAssetsHandler(assetService, platformAuthorizer, publicAuthenticator, httpapi.AssetsConfig{
|
||||
MaxJSONBytes: positiveInt64Env(getenv, "ZHINIAN_MAX_JSON_BYTES", 1<<20), MaxUploadBytes: positiveInt64Env(getenv, "ZHINIAN_MAX_UPLOAD_BYTES", 20<<20),
|
||||
MaxJSONBytes: positiveInt64Env(startupGetenv, "ZHINIAN_MAX_JSON_BYTES", 1<<20), MaxUploadBytes: positiveInt64Env(startupGetenv, "ZHINIAN_MAX_UPLOAD_BYTES", 20<<20),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
billingService := billing.NewService(billingStore, nil).SetEnabled(strings.TrimSpace(getenv("ZHINIAN_BILLING_REQUIRED")) != "0")
|
||||
runtimeSettings := defaultSettingsService(getenv)
|
||||
if config.Backend == postgres.BackendPostgres {
|
||||
runtimeSettings = databaseSettingsService(getenv, database.Store)
|
||||
}
|
||||
billingService := billing.NewService(billingStore, nil).SetEnabled(strings.TrimSpace(startupGetenv("ZHINIAN_BILLING_REQUIRED")) != "0")
|
||||
billingAccounts := settingsBillingAccountStore{service: runtimeSettings}
|
||||
templateService := templates.NewService(templateCatalog, nil, nil)
|
||||
logService := options.Logs
|
||||
|
||||
@@ -78,17 +78,40 @@ func TestApplicationRejectsInvalidProductionDatabaseConfiguration(t *testing.T)
|
||||
}
|
||||
|
||||
func TestProductionApplicationCanStartBeforeProviderSettingsAreConfigured(t *testing.T) {
|
||||
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
||||
"NODE_ENV": "production",
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgres://user:password@127.0.0.1:5432/zhinian",
|
||||
})})
|
||||
app, err := application.New(application.Options{
|
||||
Getenv: applicationEnv(map[string]string{
|
||||
"NODE_ENV": "production",
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgres://user:password@127.0.0.1:5432/zhinian",
|
||||
}),
|
||||
RuntimeSettingsRepository: applicationRuntimeSettingsRepositoryStub{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New() without provider credentials = %v", err)
|
||||
}
|
||||
app.Close()
|
||||
}
|
||||
|
||||
func TestProductionApplicationUsesDatabaseSettingsDuringStartup(t *testing.T) {
|
||||
app, err := application.New(application.Options{
|
||||
Getenv: applicationEnv(map[string]string{
|
||||
"NODE_ENV": "production",
|
||||
"ZHINIAN_DATA_BACKEND": "postgres",
|
||||
"DATABASE_URL": "postgres://user:password@127.0.0.1:5432/zhinian",
|
||||
"ZHINIAN_AUTH_REQUIRED": "1",
|
||||
"ZHINIAN_BILLING_REQUIRED": "1",
|
||||
}),
|
||||
RuntimeSettingsRepository: applicationRuntimeSettingsRepositoryStub{values: map[string]string{
|
||||
"ZHINIAN_AUTH_REQUIRED": "0",
|
||||
"ZHINIAN_BILLING_REQUIRED": "0",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New() with database startup settings = %v", err)
|
||||
}
|
||||
app.Close()
|
||||
}
|
||||
|
||||
func TestProductionLocalBackendNeverGrantsAnonymousAdministrator(t *testing.T) {
|
||||
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
|
||||
"NODE_ENV": "production",
|
||||
@@ -580,6 +603,20 @@ type applicationCredentialAuthenticator struct {
|
||||
passwords []string
|
||||
}
|
||||
|
||||
type applicationRuntimeSettingsRepositoryStub struct{ values map[string]string }
|
||||
|
||||
func (repository applicationRuntimeSettingsRepositoryStub) LoadRuntimeSettings(context.Context, []string) (map[string]string, error) {
|
||||
values := map[string]string{}
|
||||
for key, value := range repository.values {
|
||||
values[key] = value
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (applicationRuntimeSettingsRepositoryStub) SaveRuntimeSettings(context.Context, map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (authenticator *applicationCredentialAuthenticator) AttemptPasswordLogin(_ context.Context, phone, password string, _ time.Time) (identity.LoginAccount, error) {
|
||||
authenticator.phones = append(authenticator.phones, phone)
|
||||
authenticator.passwords = append(authenticator.passwords, password)
|
||||
|
||||
@@ -38,6 +38,54 @@ func TestDefaultSettingsServiceAppliesProviderSettingsWithoutRestart(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeSettingsGetenvProvidesDatabaseBackedStartupConfiguration(t *testing.T) {
|
||||
settingsPath := filepath.Join(t.TempDir(), ".env.local")
|
||||
fallbackValues := map[string]string{
|
||||
"ZHINIAN_SETTINGS_FILE": settingsPath,
|
||||
"ZHINIAN_AUTH_REQUIRED": "0",
|
||||
"ZHINIAN_AUTH_SESSION_SECRET": "environment-session-secret-that-is-long-enough",
|
||||
"ZHINIAN_BILLING_REQUIRED": "1",
|
||||
"ALI_OSS_ENDPOINT": "https://environment-oss.example.test",
|
||||
"ALI_OSS_BUCKET": "environment-bucket",
|
||||
"ALI_OSS_ACCESS_KEY_ID": "environment-access-key",
|
||||
"ALI_OSS_ACCESS_KEY_SECRET": "environment-access-secret",
|
||||
"ALI_OSS_PUBLIC_BASE_URL": "https://environment-cdn.example.test",
|
||||
"ZHINIAN_PROVIDER_TIMEOUT_MS": "1234",
|
||||
}
|
||||
fallback := func(name string) string { return fallbackValues[name] }
|
||||
repository := &applicationRuntimeSettingsRepository{values: map[string]string{
|
||||
"ZHINIAN_AUTH_REQUIRED": "1",
|
||||
"ZHINIAN_AUTH_SESSION_SECRET": "database-session-secret-that-is-long-enough",
|
||||
"ZHINIAN_BILLING_REQUIRED": "0",
|
||||
"ALI_OSS_ENDPOINT": "https://oss-cn-test.aliyuncs.com",
|
||||
"ALI_OSS_BUCKET": "database-bucket",
|
||||
"ALI_OSS_ACCESS_KEY_ID": "database-access-key",
|
||||
"ALI_OSS_ACCESS_KEY_SECRET": "database-access-secret",
|
||||
"ALI_OSS_PUBLIC_BASE_URL": "https://database-cdn.example.test",
|
||||
}}
|
||||
service := databaseSettingsService(fallback, repository)
|
||||
getenv, err := runtimeSettingsGetenv(context.Background(), fallback, service)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(repository.requestedKeys, settings.RuntimeSettingKeys()) {
|
||||
t.Fatalf("requested keys=%#v want %#v", repository.requestedKeys, settings.RuntimeSettingKeys())
|
||||
}
|
||||
auth, err := ParseAuthConfig(getenv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !auth.Required || auth.SessionSecret != "database-session-secret-that-is-long-enough" {
|
||||
t.Fatalf("auth=%#v", auth)
|
||||
}
|
||||
if getenv("ZHINIAN_BILLING_REQUIRED") != "0" || getenv("ZHINIAN_PROVIDER_TIMEOUT_MS") != "1234" {
|
||||
t.Fatalf("billing=%q timeout=%q", getenv("ZHINIAN_BILLING_REQUIRED"), getenv("ZHINIAN_PROVIDER_TIMEOUT_MS"))
|
||||
}
|
||||
if _, configured, err := configuredOSSBlobStore(getenv); err != nil || !configured {
|
||||
t.Fatalf("configured OSS = %v err=%v", configured, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingAccountAndSettingsUseOneRuntimeSource(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ".env.local")
|
||||
if err := os.WriteFile(path, []byte("ZHINIAN_BILLING_ACCOUNT_NAME=Original\nZHINIAN_BILLING_ACCOUNT_BANK=Old Bank\n"), 0o600); err != nil {
|
||||
@@ -276,9 +324,13 @@ func TestPrefixedBlobStoreKeepsApplicationStoragePathStable(t *testing.T) {
|
||||
|
||||
type recordingBlobStore struct{ putKey, readKey, deleteKey string }
|
||||
|
||||
type applicationRuntimeSettingsRepository struct{ values map[string]string }
|
||||
type applicationRuntimeSettingsRepository struct {
|
||||
values map[string]string
|
||||
requestedKeys []string
|
||||
}
|
||||
|
||||
func (repository *applicationRuntimeSettingsRepository) LoadRuntimeSettings(context.Context, []string) (map[string]string, error) {
|
||||
func (repository *applicationRuntimeSettingsRepository) LoadRuntimeSettings(_ context.Context, keys []string) (map[string]string, error) {
|
||||
repository.requestedKeys = append([]string(nil), keys...)
|
||||
values := map[string]string{}
|
||||
for key, value := range repository.values {
|
||||
values[key] = value
|
||||
|
||||
Reference in new issue
Block a user