瀏覽代碼

Fix APP__ env prefix so .env database URL is honored.

Figment was prefixed with APP_, so APP__DATABASE__URL never mapped
to database.url and the server kept logging in as the default app
role. Also walk up to the workspace .env and log a redacted DSN.
master
kashiro 1 周之前
父節點
當前提交
a9f81efe28
共有 5 個文件被更改,包括 85 次插入8 次删除
  1. +4
    -3
      .env.example
  2. +41
    -3
      apps/server/src/config.rs
  3. +5
    -1
      apps/server/src/main.rs
  4. +27
    -0
      apps/server/tests/config.rs
  5. +8
    -1
      docs/runbook.md

+ 4
- 3
.env.example 查看文件

@@ -2,9 +2,10 @@

APP_ENV=development

# Overrides config/*.toml. Nested keys use a double underscore.
# Overrides config/*.toml. Nested keys use a double underscore after APP.
# The running server reads APP__DATABASE__URL, not DATABASE_URL.
# APP__SERVER__PORT=8080
# APP__DATABASE__URL=postgres://app:app@127.0.0.1:5432/app
APP__DATABASE__URL=postgres://app:app@127.0.0.1:5432/app

# Used by sqlx-cli if you install it.
# Used by sqlx-cli if you install it. The Axum process ignores this key.
DATABASE_URL=postgres://app:app@127.0.0.1:5432/app

+ 41
- 3
apps/server/src/config.rs 查看文件

@@ -12,7 +12,11 @@ use serde::{Deserialize, Serialize};
/// 1. struct defaults
/// 2. `config/default.toml`
/// 3. `config/{APP_ENV}.toml` (`development` when unset)
/// 4. `APP__SECTION__KEY` environment variables
/// 4. `config/local.toml` (gitignored)
/// 5. `APP__SECTION__KEY` environment variables
///
/// Figment prefix is `APP__` (two underscores). `APP__DATABASE__URL` maps to
/// `database.url`. `APP_DATABASE__URL` does not.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub app: AppSection,
@@ -106,14 +110,15 @@ impl Default for AppConfig {

impl AppConfig {
pub fn load() -> Result<Self, Box<figment::Error>> {
let _ = dotenvy::dotenv();
load_dotenv();
let env = current_env();
let config_dir = discover_config_dir();

Figment::from(Serialized::defaults(Self::default()))
.merge(Toml::file(config_dir.join("default.toml")))
.merge(Toml::file(config_dir.join(format!("{env}.toml"))))
.merge(Env::prefixed("APP_").split("__"))
.merge(Toml::file(config_dir.join("local.toml")))
.merge(Env::prefixed("APP__").split("__"))
.extract()
.map_err(Box::new)
}
@@ -135,6 +140,39 @@ pub fn current_env() -> String {
std::env::var("APP_ENV").unwrap_or_else(|_| "development".into())
}

/// Walk from cwd / crate dir so RustRover can start the binary from `apps/server`.
fn load_dotenv() {
let mut candidates = Vec::new();
if let Ok(cwd) = std::env::current_dir() {
candidates.extend(cwd.ancestors().map(|dir| dir.join(".env")));
}
if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
let manifest = PathBuf::from(manifest);
candidates.push(manifest.join(".env"));
candidates.push(manifest.join("../../.env"));
}

for path in candidates {
if path.is_file() {
let _ = dotenvy::from_path(&path);
return;
}
}
}

/// Drop the password so logs can show which role/host we actually used.
pub fn redact_db_url(url: &str) -> String {
let Some(scheme_end) = url.find("://") else {
return url.to_string();
};
let rest = &url[scheme_end + 3..];
let Some(at) = rest.find('@') else {
return url.to_string();
};
let user = rest[..at].split(':').next().unwrap_or("");
format!("{}{user}{}", &url[..=scheme_end + 2], &rest[at..])
}

/// Walks from cwd (and the server crate dir) until `config/default.toml` is found.
pub fn discover_config_dir() -> PathBuf {
if let Ok(explicit) = std::env::var("APP_CONFIG_DIR") {


+ 5
- 1
apps/server/src/main.rs 查看文件

@@ -19,7 +19,11 @@ async fn main() -> anyhow::Result<()> {
);

// 3. Database + migrations
let pool = db::connect(&config).await?;
let database = server::config::redact_db_url(&config.database.url);
tracing::info!(%database, "connecting");
let pool = db::connect(&config)
.await
.map_err(|err| anyhow::anyhow!("database connect failed ({database}): {err}"))?;
if config.database.run_migrations {
db::migrate(&pool).await?;
tracing::info!("migrations applied");


+ 27
- 0
apps/server/tests/config.rs 查看文件

@@ -1,4 +1,9 @@
use std::sync::Mutex;

use server::AppConfig;
use server::config::redact_db_url;

static ENV_LOCK: Mutex<()> = Mutex::new(());

#[test]
fn default_config_binds_loopback() {
@@ -10,8 +15,30 @@ fn default_config_binds_loopback() {

#[test]
fn layered_config_loads_from_workspace() {
let _guard = ENV_LOCK.lock().expect("env lock");
let config = AppConfig::load().expect("load config from workspace");
assert_eq!(config.server.host, "127.0.0.1");
assert_eq!(config.frontend.host, "127.0.0.1");
assert!(!config.database.url.is_empty());
}

#[test]
fn app_double_underscore_overrides_database_url() {
let _guard = ENV_LOCK.lock().expect("env lock");
unsafe {
std::env::set_var("APP__DATABASE__URL", "postgres://jared:secret@127.0.0.1:5432/mydb");
}
let config = AppConfig::load().expect("load config with env override");
unsafe {
std::env::remove_var("APP__DATABASE__URL");
}
assert_eq!(config.database.url, "postgres://jared:secret@127.0.0.1:5432/mydb");
}

#[test]
fn redact_db_url_strips_password() {
assert_eq!(
redact_db_url("postgres://app:hunter2@127.0.0.1:5432/app"),
"postgres://app@127.0.0.1:5432/app"
);
}

+ 8
- 1
docs/runbook.md 查看文件

@@ -44,4 +44,11 @@ Vite also regenerates on dev/build if `packages/contracts/openapi.json` changes.

Default: `postgres://app:app@127.0.0.1:5432/app`

Override with `APP__DATABASE__URL` or `config/local.toml` (gitignored).
That user only exists if you start the bundled compose file. For a local cluster, override with `APP__DATABASE__URL` (two underscores after `APP`) or `config/local.toml`:

```toml
[database]
url = "postgres://YOUR_USER:YOUR_PASS@127.0.0.1:5432/YOUR_DB"
```

`DATABASE_URL` is for sqlx-cli only. The server does not read it.

Loading…
取消
儲存