mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 13:14:15 +08:00
fix(config): 修复 .env 加载与密码初始化
- 统一开发脚本、Vite 代理与 Docker Compose 的 HOST/PORT 配置
- 首次启动按字面值读取 AUTH_PASSWORD,避免 ${VAR} 被环境变量插值
- 已有 auth.json 时继续以 Web 端密码为准,不受 .env 覆盖
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@ LOG_LEVEL=INFO
|
||||
# 首次启动时预置访问密码(可选)。公网服务器部署时填入,免去 SSH 端口转发设密码。
|
||||
# 仅在尚未设置密码时生效(一次性初始化);设过后改密码请用页面 UI, 此处不再读取。
|
||||
# 建议至少 6 位。.env 文件权限保持 600 且不要提交到 Git。
|
||||
AUTH_PASSWORD=
|
||||
AUTH_PASSWORD=''
|
||||
|
||||
# Optional backend dependency extras for Docker and ./dev.sh / .\dev.ps1.
|
||||
# Set to legacy-cpu on older CPUs without AVX2/FMA support.
|
||||
|
||||
+2
-1
@@ -125,7 +125,8 @@ COPY --from=stocksdk-builder /build/node_modules ./app/plugins/stocksdk/node_mod
|
||||
COPY tiers.yaml /app/tiers.yaml
|
||||
ENV STATIC_DIR=/app/static \
|
||||
TIERS_YAML=/app/tiers.yaml \
|
||||
DATA_DIR=/app/data
|
||||
DATA_DIR=/app/data \
|
||||
TICKFLOW_ENV_FILE=/app/.env
|
||||
|
||||
# Frontend 静态产物
|
||||
COPY --from=frontend-builder /build/dist ./static
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""全局配置 — 从环境变量 / .env 读取。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -63,11 +64,17 @@ def _project_root() -> Path:
|
||||
|
||||
_PROJECT_ROOT = _project_root()
|
||||
_RESOURCE_ROOT = _resource_root()
|
||||
_ENV_FILE = Path(
|
||||
os.environ.get(
|
||||
"TICKFLOW_ENV_FILE",
|
||||
str(_RESOURCE_ROOT / ".env") if not _IS_FROZEN else ".env",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=str(_RESOURCE_ROOT / ".env") if not _IS_FROZEN else ".env",
|
||||
env_file=str(_ENV_FILE),
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
@@ -131,9 +131,17 @@ def bootstrap_from_env() -> bool:
|
||||
Returns:
|
||||
True 表示本次用环境变量初始化了密码; False 表示无需初始化。
|
||||
"""
|
||||
from app.config import settings
|
||||
from app.config import _ENV_FILE, settings
|
||||
|
||||
pwd = (settings.auth_password or "").strip()
|
||||
# Compose 会对 env_file 中未加单引号的 $VAR 做插值。Docker 部署时同时
|
||||
# 只读挂载原始 .env,首次初始化密码直接按 dotenv 语义读取,避免特殊字符被截断。
|
||||
if _ENV_FILE.is_file():
|
||||
from dotenv import dotenv_values
|
||||
|
||||
raw_pwd = dotenv_values(_ENV_FILE, encoding="utf-8", interpolate=False).get("AUTH_PASSWORD")
|
||||
if isinstance(raw_pwd, str) and raw_pwd.strip():
|
||||
pwd = raw_pwd.strip()
|
||||
if not pwd:
|
||||
return False
|
||||
if is_configured():
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from app import config as app_config
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_auth_store(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> Iterator[tuple[ModuleType, Path, Path]]:
|
||||
monkeypatch.setattr(app_config.settings, "data_dir", tmp_path)
|
||||
from app.services import auth
|
||||
|
||||
auth_path = tmp_path / "user_data" / "auth.json"
|
||||
env_path = tmp_path / ".env"
|
||||
monkeypatch.setattr(app_config, "_ENV_FILE", env_path)
|
||||
monkeypatch.setattr(app_config.settings, "auth_password", "")
|
||||
auth._sessions.clear()
|
||||
auth._configured_cache = None
|
||||
yield auth, auth_path, env_path
|
||||
auth._sessions.clear()
|
||||
auth._configured_cache = None
|
||||
|
||||
|
||||
def test_bootstrap_recovers_compose_interpolated_password_from_raw_env(
|
||||
isolated_auth_store: tuple[ModuleType, Path, Path],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
auth, auth_path, env_path = isolated_auth_store
|
||||
password = "pw${special}-secret"
|
||||
env_path.write_text(
|
||||
f"AUTH_PASSWORD={password}\nDATA_DIR={tmp_path}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
configured = Settings(_env_file=env_path)
|
||||
configured.auth_password = "pw-secret" # 模拟 Compose 将未定义的 ${special} 插值为空串
|
||||
monkeypatch.setattr(app_config, "settings", configured)
|
||||
|
||||
assert auth.bootstrap_from_env() is True
|
||||
assert auth_path.exists()
|
||||
assert password not in auth_path.read_text(encoding="utf-8")
|
||||
assert auth.verify_and_create_session(password) is not None
|
||||
assert auth.verify_and_create_session("pw-secret") is None
|
||||
|
||||
|
||||
def test_bootstrap_from_env_does_not_override_existing_password(
|
||||
isolated_auth_store: tuple[ModuleType, Path, Path],
|
||||
) -> None:
|
||||
auth, auth_path, env_path = isolated_auth_store
|
||||
auth.set_password("web-managed-secret")
|
||||
before = auth_path.read_bytes()
|
||||
env_path.write_text("AUTH_PASSWORD=replacement-secret\n", encoding="utf-8")
|
||||
app_config.settings.auth_password = "replacement-secret"
|
||||
|
||||
assert auth.bootstrap_from_env() is False
|
||||
assert auth_path.read_bytes() == before
|
||||
assert auth.verify_and_create_session("web-managed-secret") is not None
|
||||
assert auth.verify_and_create_session("replacement-secret") is None
|
||||
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
def test_settings_reads_server_and_auth_values_from_env(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
for name in ("HOST", "PORT", "LOG_LEVEL", "AUTH_PASSWORD"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_text(
|
||||
"HOST=127.0.0.1\n"
|
||||
"PORT=4318\n"
|
||||
"LOG_LEVEL=DEBUG\n"
|
||||
"AUTH_PASSWORD=config-secret\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
configured = Settings(_env_file=env_path)
|
||||
|
||||
assert configured.host == "127.0.0.1"
|
||||
assert configured.port == 4318
|
||||
assert configured.log_level == "DEBUG"
|
||||
assert configured.auth_password == "config-secret"
|
||||
@@ -18,8 +18,42 @@ param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Port precedence: CLI arg > env var > default
|
||||
if ($BackendPort -le 0) { $BackendPort = if ($env:BACKEND_PORT) { [int]$env:BACKEND_PORT } else { 3018 } }
|
||||
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$BackendDir = Join-Path $Root 'backend'
|
||||
$FrontendDir = Join-Path $Root 'frontend'
|
||||
$EnvFile = Join-Path $Root '.env'
|
||||
|
||||
# Read only launcher-owned keys. Do not execute .env as PowerShell code.
|
||||
function Read-DotEnvValue($Path, $Name) {
|
||||
if (-not (Test-Path $Path)) { return $null }
|
||||
$escaped = [Regex]::Escape($Name)
|
||||
foreach ($line in Get-Content $Path) {
|
||||
if ($line -match "^\s*$escaped\s*=\s*(.*?)\s*$") {
|
||||
$value = $Matches[1].Trim()
|
||||
$value = ($value -replace '\s+#.*$', '').Trim()
|
||||
if ($value.Length -ge 2 -and
|
||||
(($value.StartsWith('"') -and $value.EndsWith('"')) -or
|
||||
($value.StartsWith("'") -and $value.EndsWith("'")))) {
|
||||
return $value.Substring(1, $value.Length - 2)
|
||||
}
|
||||
return $value
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$DotEnvHost = Read-DotEnvValue $EnvFile 'HOST'
|
||||
$DotEnvPort = Read-DotEnvValue $EnvFile 'PORT'
|
||||
$BindAddress = if ($env:HOST) { $env:HOST } elseif ($DotEnvHost) { $DotEnvHost } else { '0.0.0.0' }
|
||||
$DisplayHost = if ($BindAddress -in @('0.0.0.0', '::')) { 'localhost' } else { $BindAddress }
|
||||
|
||||
# Port precedence: CLI arg > BACKEND_PORT env > PORT env > .env PORT > default
|
||||
if ($BackendPort -le 0) {
|
||||
if ($env:BACKEND_PORT) { $BackendPort = [int]$env:BACKEND_PORT }
|
||||
elseif ($env:PORT) { $BackendPort = [int]$env:PORT }
|
||||
elseif ($DotEnvPort) { $BackendPort = [int]$DotEnvPort }
|
||||
else { $BackendPort = 3018 }
|
||||
}
|
||||
if ($FrontendPort -le 0) { $FrontendPort = if ($env:FRONTEND_PORT) { [int]$env:FRONTEND_PORT } else { 3011 } }
|
||||
|
||||
# Force UTF-8 console output so child process logs aren't garbled
|
||||
@@ -28,10 +62,6 @@ try {
|
||||
$OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
} catch {}
|
||||
|
||||
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$BackendDir = Join-Path $Root 'backend'
|
||||
$FrontendDir = Join-Path $Root 'frontend'
|
||||
|
||||
function Log-Info($m) { Write-Host "[dev] $m" -ForegroundColor DarkGray }
|
||||
function Log-Ok ($m) { Write-Host "[dev] $m" -ForegroundColor Green }
|
||||
function Log-Warn($m) { Write-Host "[dev] $m" -ForegroundColor Yellow }
|
||||
@@ -113,15 +143,7 @@ Free-Port 'frontend' $FrontendPort
|
||||
# select Polars' rtcompat runtime before the backend starts.
|
||||
$BackendExtras = $env:BACKEND_EXTRAS
|
||||
if (-not (Test-Path Env:BACKEND_EXTRAS)) {
|
||||
$envFile = Join-Path $Root '.env'
|
||||
if (Test-Path $envFile) {
|
||||
foreach ($line in Get-Content $envFile) {
|
||||
if ($line -match '^\s*BACKEND_EXTRAS\s*=\s*(.*?)\s*$') {
|
||||
$BackendExtras = $Matches[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
$BackendExtras = Read-DotEnvValue $EnvFile 'BACKEND_EXTRAS'
|
||||
}
|
||||
|
||||
$BackendExtraArgs = @()
|
||||
@@ -156,8 +178,8 @@ Write-Host ''
|
||||
Write-Host '+----------------------------------------------+' -ForegroundColor Blue
|
||||
Write-Host '| tickflow-stock-panel |' -ForegroundColor Blue
|
||||
Write-Host '| |' -ForegroundColor Blue
|
||||
Write-Host "| backend http://localhost:$BackendPort" -ForegroundColor Blue
|
||||
Write-Host "| frontend http://localhost:$FrontendPort" -ForegroundColor Blue
|
||||
Write-Host "| backend http://${DisplayHost}:$BackendPort" -ForegroundColor Blue
|
||||
Write-Host "| frontend http://${DisplayHost}:$FrontendPort" -ForegroundColor Blue
|
||||
Write-Host '| |' -ForegroundColor Blue
|
||||
Write-Host '| Ctrl-C closes both |' -ForegroundColor Blue
|
||||
Write-Host '+----------------------------------------------+' -ForegroundColor Blue
|
||||
@@ -170,7 +192,7 @@ $backendPidFile = [System.IO.Path]::GetTempFileName()
|
||||
$frontendPidFile = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
$backendJob = Start-Job -Name 'backend' -ScriptBlock {
|
||||
param($pidFile, $dir, $port)
|
||||
param($pidFile, $dir, $envFile, $bindAddress, $port)
|
||||
# Start-Job 开的是全新 powershell.exe 子进程, 不继承主进程的 UTF-8 设置,
|
||||
# 默认用系统 ANSI (中文 Windows = GBK/cp936) 解码后端 UTF-8 输出 → 中文乱码。
|
||||
# 这里强制子进程用 UTF-8, 与 app/__init__.py 的 stdout/stderr 编码对齐。
|
||||
@@ -179,18 +201,21 @@ $backendJob = Start-Job -Name 'backend' -ScriptBlock {
|
||||
$PID | Out-File -FilePath $pidFile -Encoding ascii -Force
|
||||
$env:PYTHONUNBUFFERED = '1'
|
||||
Set-Location $dir
|
||||
& .\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --host 0.0.0.0 --port $port 2>&1
|
||||
} -ArgumentList $backendPidFile, $BackendDir, $BackendPort
|
||||
$envArgs = if (Test-Path $envFile) { @('--env-file', $envFile) } else { @() }
|
||||
& .\.venv\Scripts\python.exe -m uvicorn app.main:app @envArgs --reload --host $bindAddress --port $port 2>&1
|
||||
} -ArgumentList $backendPidFile, $BackendDir, $EnvFile, $BindAddress, $BackendPort
|
||||
|
||||
$frontendJob = Start-Job -Name 'frontend' -ScriptBlock {
|
||||
param($pidFile, $dir, $port)
|
||||
param($pidFile, $dir, $bindAddress, $backendPort, $port)
|
||||
# 同上: job 子进程默认 GBK, pnpm/前端工具链也是 UTF-8 输出, 需对齐。
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
$OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
$PID | Out-File -FilePath $pidFile -Encoding ascii -Force
|
||||
Set-Location $dir
|
||||
& pnpm dev --host 0.0.0.0 --port $port 2>&1
|
||||
} -ArgumentList $frontendPidFile, $FrontendDir, $FrontendPort
|
||||
$env:BACKEND_HOST = $bindAddress
|
||||
$env:BACKEND_PORT = [string]$backendPort
|
||||
& pnpm dev --host $bindAddress --port $port 2>&1
|
||||
} -ArgumentList $frontendPidFile, $FrontendDir, $BindAddress, $BackendPort, $FrontendPort
|
||||
|
||||
# Wait up to 5 seconds for the PID files to materialise
|
||||
function Read-JobPid($file) {
|
||||
|
||||
@@ -13,13 +13,48 @@ set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKEND_DIR="$ROOT/backend"
|
||||
FRONTEND_DIR="$ROOT/frontend"
|
||||
BACKEND_PORT="${BACKEND_PORT:-3018}"
|
||||
|
||||
# Read only the launcher-owned keys from .env. Do not source the whole file:
|
||||
# .env is data, not a shell script, and may contain values that are unsafe or
|
||||
# invalid as Bash syntax. Exported environment variables keep highest priority.
|
||||
read_dotenv_value() {
|
||||
local key="$1"
|
||||
if [[ ! -f "$ROOT/.env" ]]; then
|
||||
return 0
|
||||
fi
|
||||
awk -v wanted="$key" '
|
||||
$0 ~ "^[[:space:]]*" wanted "[[:space:]]*=" {
|
||||
sub(/^[^=]*=/, "")
|
||||
sub(/[[:space:]]+#.*$/, "")
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
|
||||
if (($0 ~ /^".*"$/) || ($0 ~ /^\047.*\047$/)) {
|
||||
$0 = substr($0, 2, length($0) - 2)
|
||||
}
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$ROOT/.env"
|
||||
}
|
||||
|
||||
ENV_HOST="$(read_dotenv_value HOST)"
|
||||
ENV_PORT="$(read_dotenv_value PORT)"
|
||||
BACKEND_HOST="${HOST:-${ENV_HOST:-0.0.0.0}}"
|
||||
# Keep BACKEND_PORT as a backwards-compatible explicit override.
|
||||
BACKEND_PORT="${BACKEND_PORT:-${PORT:-${ENV_PORT:-3018}}}"
|
||||
FRONTEND_PORT="${FRONTEND_PORT:-3011}"
|
||||
UVICORN_ENV_ARGS=()
|
||||
if [[ -f "$ROOT/.env" ]]; then
|
||||
UVICORN_ENV_ARGS=(--env-file "$ROOT/.env")
|
||||
fi
|
||||
DISPLAY_HOST="$BACKEND_HOST"
|
||||
if [[ "$DISPLAY_HOST" == "0.0.0.0" || "$DISPLAY_HOST" == "::" ]]; then
|
||||
DISPLAY_HOST="localhost"
|
||||
fi
|
||||
|
||||
# Match Docker's BACKEND_EXTRAS behavior so old CPUs can select Polars'
|
||||
# rtcompat runtime before the backend starts. An exported value wins over .env.
|
||||
if [[ -z "${BACKEND_EXTRAS+x}" && -f "$ROOT/.env" ]]; then
|
||||
BACKEND_EXTRAS="$(awk '/^[[:space:]]*BACKEND_EXTRAS[[:space:]]*=/ {sub(/^[^=]*=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' "$ROOT/.env")"
|
||||
BACKEND_EXTRAS="$(read_dotenv_value BACKEND_EXTRAS)"
|
||||
fi
|
||||
BACKEND_EXTRAS="${BACKEND_EXTRAS:-}"
|
||||
BACKEND_EXTRA_ARGS=()
|
||||
@@ -129,8 +164,8 @@ echo
|
||||
echo -e "${BLUE}╭──────────────────────────────────────────────╮${NC}"
|
||||
echo -e "${BLUE}│${NC} ${GREEN}tickflow-stock-panel${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} backend ${YELLOW}http://localhost:$BACKEND_PORT${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} frontend ${YELLOW}http://localhost:$FRONTEND_PORT${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} backend ${YELLOW}http://$DISPLAY_HOST:$BACKEND_PORT${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} frontend ${YELLOW}http://$DISPLAY_HOST:$FRONTEND_PORT${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}│${NC} Ctrl-C 同时关闭两端 ${BLUE}│${NC}"
|
||||
echo -e "${BLUE}╰──────────────────────────────────────────────╯${NC}"
|
||||
@@ -138,14 +173,16 @@ echo
|
||||
|
||||
(
|
||||
cd "$BACKEND_DIR"
|
||||
uv run uvicorn app.main:app --reload --host 0.0.0.0 --port "$BACKEND_PORT" 2>&1 \
|
||||
uv run uvicorn app.main:app "${UVICORN_ENV_ARGS[@]}" --reload \
|
||||
--host "$BACKEND_HOST" --port "$BACKEND_PORT" 2>&1 \
|
||||
| prefix_awk "$(printf "${BLUE}[backend ]${NC} ")"
|
||||
) &
|
||||
PIDS+=("$!")
|
||||
|
||||
(
|
||||
cd "$FRONTEND_DIR"
|
||||
pnpm dev --host 0.0.0.0 --port "$FRONTEND_PORT" 2>&1 \
|
||||
BACKEND_HOST="$BACKEND_HOST" BACKEND_PORT="$BACKEND_PORT" \
|
||||
pnpm dev --host "$BACKEND_HOST" --port "$FRONTEND_PORT" 2>&1 \
|
||||
| prefix_awk "$(printf "${GREEN}[frontend]${NC} ")"
|
||||
) &
|
||||
PIDS+=("$!")
|
||||
|
||||
+3
-1
@@ -10,7 +10,7 @@ services:
|
||||
CODEX_CLI_VERSION: ${CODEX_CLI_VERSION:-0.144.3}
|
||||
container_name: TickFlow_Stock_Panel
|
||||
ports:
|
||||
- "${PORT:-3018}:3018"
|
||||
- "${HOST:-0.0.0.0}:${PORT:-3018}:3018"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
env_file:
|
||||
@@ -26,6 +26,8 @@ services:
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./tiers.yaml:/app/tiers.yaml:ro
|
||||
# 保留原始 dotenv 值供首次密码初始化读取,避免 Compose 展开密码中的 $VAR。
|
||||
- ./.env:/app/.env:ro
|
||||
# 复用主机 Codex 登录态;后端只读后复制到单次请求的临时 CODEX_HOME。
|
||||
# Windows PowerShell/CMD 下 HOME 常未设置, 可通过 .env 里 CODEX_HOME_HOST 覆盖。
|
||||
- ${CODEX_HOME_HOST:-${HOME}/.codex}:/root/.codex:ro
|
||||
|
||||
@@ -58,13 +58,13 @@ AI_DAILY_TOKEN_BUDGET=500000 # 每日 token 预算上限
|
||||
## 服务
|
||||
|
||||
```ini
|
||||
HOST=0.0.0.0 # 监听地址
|
||||
PORT=3018 # 服务端口
|
||||
HOST=0.0.0.0 # 开发服务监听地址 / Docker 主机绑定地址
|
||||
PORT=3018 # 开发后端端口 / Docker 主机映射端口
|
||||
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR
|
||||
```
|
||||
|
||||
- `HOST`:`0.0.0.0` 监听所有网卡(容器/公网部署需要);仅本机用可设 `127.0.0.1`
|
||||
- `PORT`:默认 `3018`,改端口后 Docker 映射、SSH 转发命令里的端口也要同步改
|
||||
- `PORT`:默认 `3018`;开发模式兼容显式的 `BACKEND_PORT` 覆盖,改端口后 SSH 转发命令也要同步改
|
||||
- `LOG_LEVEL`:排查问题时改 `DEBUG`
|
||||
|
||||
---
|
||||
@@ -84,10 +84,11 @@ DATA_DIR=./data # Parquet / DuckDB 数据存储目录
|
||||
## 访问密码(公网部署)
|
||||
|
||||
```ini
|
||||
AUTH_PASSWORD=你的密码 # 至少 6 位;仅首次生效,已设过则不覆盖
|
||||
AUTH_PASSWORD='你的密码' # 至少 6 位;仅首次生效,已设过则不覆盖
|
||||
```
|
||||
|
||||
面板首次设置访问密码时,出于安全考虑**仅允许本机或内网访问**(防公网陌生人抢先设置锁死面板)。公网服务器部署可通过此环境变量预置首个密码。
|
||||
密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;Docker 启动时也会只读挂载原始 `.env`,兼容已有的未加引号配置。
|
||||
|
||||
详细步骤、SSH 转发方案、重置密码方法见 [deployment.md → 访问密码设置](./deployment.md#访问密码设置公网部署必读)。
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
```bash
|
||||
# 编辑服务器上的 .env (通常在项目根目录或 backend/ 下)
|
||||
AUTH_PASSWORD=你的密码
|
||||
AUTH_PASSWORD='你的密码'
|
||||
```
|
||||
|
||||
然后重启服务。启动时会自动:
|
||||
@@ -30,6 +30,7 @@ AUTH_PASSWORD=你的密码
|
||||
|
||||
- **密码至少 6 位**,否则会被跳过并记一条 warning 日志
|
||||
- **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码)
|
||||
- 密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;启动时也会从只读挂载的原始 `.env` 初始化,兼容已有的未加引号配置
|
||||
- `.env` 文件权限保持 `600`,**不要提交到 Git**
|
||||
- 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json`
|
||||
|
||||
|
||||
+2
-1
@@ -123,7 +123,7 @@ git pull
|
||||
在 `.env` 文件(或 Docker / 系统环境变量)里设置 `AUTH_PASSWORD`:
|
||||
|
||||
```bash
|
||||
AUTH_PASSWORD=你的密码
|
||||
AUTH_PASSWORD='你的密码'
|
||||
```
|
||||
|
||||
然后重启服务。启动时会自动:
|
||||
@@ -138,6 +138,7 @@ AUTH_PASSWORD=你的密码
|
||||
|
||||
- **密码至少 6 位**,否则会被跳过并记一条 warning 日志
|
||||
- **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码)
|
||||
- 密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;启动时也会从只读挂载的原始 `.env` 初始化,兼容已有的未加引号配置
|
||||
- `.env` 文件权限保持 `600`,**不要提交到 Git**
|
||||
- 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 后端 API 客户端 — 全项目统一入口
|
||||
//
|
||||
// Dev:Vite 代理 /api 到 :3018
|
||||
// Dev: Vite 按启动脚本解析出的 BACKEND_HOST/BACKEND_PORT 代理 /api
|
||||
// Prod:同源(FastAPI 托管前端 dist)
|
||||
|
||||
import { toast } from '@/components/Toast'
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'node:path';
|
||||
const backendHost = process.env.BACKEND_HOST || '127.0.0.1';
|
||||
const proxyHost = ['0.0.0.0', '::'].includes(backendHost) ? '127.0.0.1' : backendHost;
|
||||
const backendPort = process.env.BACKEND_PORT || '3018';
|
||||
const backendTarget = `http://${proxyHost}:${backendPort}`;
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
@@ -9,12 +13,12 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // 允许局域网访问
|
||||
host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖
|
||||
port: 3011,
|
||||
proxy: {
|
||||
// dev 时 /api 转发到 FastAPI
|
||||
// dev 时 /api 转发到与启动脚本相同的 FastAPI 地址
|
||||
'/api': {
|
||||
target: 'http://localhost:3018',
|
||||
target: backendTarget,
|
||||
// SSE 端点需要禁用缓冲
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
@@ -26,7 +30,7 @@ export default defineConfig({
|
||||
});
|
||||
},
|
||||
},
|
||||
'/health': 'http://localhost:3018',
|
||||
'/health': backendTarget,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'node:path'
|
||||
|
||||
const backendHost = process.env.BACKEND_HOST || '127.0.0.1'
|
||||
const proxyHost = ['0.0.0.0', '::'].includes(backendHost) ? '127.0.0.1' : backendHost
|
||||
const backendPort = process.env.BACKEND_PORT || '3018'
|
||||
const backendTarget = `http://${proxyHost}:${backendPort}`
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
@@ -10,12 +15,12 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // 允许局域网访问
|
||||
host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖
|
||||
port: 3011,
|
||||
proxy: {
|
||||
// dev 时 /api 转发到 FastAPI
|
||||
// dev 时 /api 转发到与启动脚本相同的 FastAPI 地址
|
||||
'/api': {
|
||||
target: 'http://localhost:3018',
|
||||
target: backendTarget,
|
||||
// SSE 端点需要禁用缓冲
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
@@ -27,7 +32,7 @@ export default defineConfig({
|
||||
})
|
||||
},
|
||||
},
|
||||
'/health': 'http://localhost:3018',
|
||||
'/health': backendTarget,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user