diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7795592..8a8e1b1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-dev.txt + pip install -r requirements.lock -r requirements-dev.lock - name: Ruff run: python -m ruff check . diff --git a/.gitignore b/.gitignore index 25cde8b6..a5c3d391 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,11 @@ Thumbs.db frontend/node_modules/ frontend/.next/ frontend/.vercel/ +frontend/.env +frontend/.env.local +frontend/.env.production +frontend/.env.* +!frontend/.env.example frontend/*.tsbuildinfo frontend/.codex-next-dev*.log frontend/.codex-next-start*.log diff --git a/Dockerfile b/Dockerfile index 41255e07..2d05e79b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ gcc libhdf5-dev libnetcdf-dev && \ rm -rf /var/lib/apt/lists/* -COPY requirements.txt . +COPY requirements.lock . RUN --mount=type=cache,target=/root/.cache/pip \ - pip install --prefer-binary -r requirements.txt + pip install --prefer-binary -r requirements.lock COPY . . diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..b53020fa --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,12 @@ +.git +.next +node_modules + +.env +.env.local +.env.production +.env.* +!.env.example + +*.log +*.tsbuildinfo diff --git a/frontend/.env.local b/frontend/.env.local deleted file mode 100644 index e3feb295..00000000 --- a/frontend/.env.local +++ /dev/null @@ -1,52 +0,0 @@ -# PolyWeather 前端最小配置(本地 / Vercel) -# 只部署天气看板时,先填下面 4 项即可。 - -# 必填:后端 FastAPI 基础地址 -# 默认供 Next.js API Route 在服务端代理后端使用。 -POLYWEATHER_API_BASE_URL=http://127.0.0.1:8000 - -# 可选:浏览器直连后端 FastAPI 基础地址。 -# 在 Vercel 免费额度下建议配置为 VPS HTTPS 域名,让 AI / METAR / scan 等 -# 长耗时请求绕过 Vercel Functions / Fluid Compute。 -# 例如:https://api.example.com -# 本地开发时注释掉,让 Next.js API Route 代理请求,避免 CORS 问题 -# NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=http://38.54.27.70:8080 - -# 必填:Supabase 前端公钥(鉴权开启时必须) -NEXT_PUBLIC_SUPABASE_URL=https://bttgfgupldyowkdhriqb.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_1z0DR7nZ1Juf_HGTASA8WA_uxcHJnby - -# 必填:生产环境站点 URL(OAuth 回调强制使用此域名) -# 设置后,所有登录回调将始终跳转到此域名,而非当前浏览器地址。 -# 生产环境必须设为 https://polyweather.top -NEXT_PUBLIC_SITE_URL=https://polyweather.top - -# 常用:前端鉴权开关 -# true: 启用 Supabase 登录 -# false: 关闭登录能力,访客模式 -POLYWEATHER_AUTH_ENABLED=true - -# 常用:是否强制登录 -# true: middleware 强制登录后才能访问主页面 -# false: 登录可选,访客可浏览 -POLYWEATHER_AUTH_REQUIRED=true - -# 关闭本地开发鉴权绕过 -NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false - -# 可选:分享式看板访问令牌 -# 设置后,可通过 /?access_token= 打开受保护看板 -POLYWEATHER_DASHBOARD_ACCESS_TOKEN= - -# 可选:前端 API Route 转发到后端时附带的共享令牌 -# 仅当后端启用了 entitlement / 订阅校验时需要 -POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= - -# 可选:钱包支付 / Telegram 入口 -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=8ce6332b57a42e606cf28224e82d1e02 -NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com -NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather.top,www.polyweather.top -POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com -NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group -NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/polyyuanbot -NEXT_PUBLIC_TELEGRAM_LOGIN_BOT_USERNAME=polyyuanbot diff --git a/frontend/.env.production b/frontend/.env.production deleted file mode 100644 index 5eadb7cd..00000000 --- a/frontend/.env.production +++ /dev/null @@ -1,8 +0,0 @@ -NEXT_PUBLIC_SUPABASE_URL=https://bttgfgupldyowkdhriqb.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_1z0DR7nZ1Juf_HGTASA8WA_uxcHJnby -NEXT_PUBLIC_SITE_URL=https://polyweather.top -NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=https://api.polyweather.top -NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false -NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather.top,www.polyweather.top -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=8ce6332b57a42e606cf28224e82d1e02 -NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com diff --git a/frontend/.gitignore b/frontend/.gitignore index e985853e..4d72752f 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1 +1,6 @@ .vercel +.env +.env.local +.env.production +.env.* +!.env.example diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 00000000..651f4b48 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,48 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python-version 3.11 --universal --constraint requirements.lock --output-file requirements-dev.lock requirements-dev.txt +anyio==4.13.0 + # via + # -c requirements.lock + # httpx +certifi==2026.5.20 + # via + # -c requirements.lock + # httpcore + # httpx +colorama==0.4.6 ; sys_platform == 'win32' + # via + # -c requirements.lock + # pytest +h11==0.16.0 + # via + # -c requirements.lock + # httpcore +httpcore==1.0.9 + # via + # -c requirements.lock + # httpx +httpx==0.28.1 + # via + # -c requirements.lock + # -r requirements-dev.txt +idna==3.18 + # via + # -c requirements.lock + # anyio + # httpx +iniconfig==2.3.0 + # via pytest +packaging==26.2 + # via pytest +pluggy==1.6.0 + # via pytest +pygments==2.20.0 + # via pytest +pytest==8.4.2 + # via -r requirements-dev.txt +ruff==0.15.17 + # via -r requirements-dev.txt +typing-extensions==4.15.0 ; python_full_version < '3.13' + # via + # -c requirements.lock + # anyio diff --git a/requirements-dev.txt b/requirements-dev.txt index aa18872e..6995738f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,3 @@ -pytest -ruff -httpx +pytest>=8.4,<9 +ruff>=0.12,<1 +httpx>=0.28,<1 diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 00000000..4399c56d --- /dev/null +++ b/requirements.lock @@ -0,0 +1,204 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python-version 3.11 --universal --output-file requirements.lock requirements.txt +aiohappyeyeballs==2.6.2 + # via aiohttp +aiohttp==3.14.1 + # via + # pytelegrambotapi + # web3 +aiosignal==1.4.0 + # via aiohttp +annotated-doc==0.0.4 + # via fastapi +annotated-types==0.7.0 + # via pydantic +anyio==4.13.0 + # via + # httpx + # starlette +async-timeout==5.0.1 ; python_full_version < '3.11.3' + # via redis +attrs==26.1.0 + # via aiohttp +bitarray==3.8.1 + # via eth-account +certifi==2026.5.20 + # via + # httpcore + # httpx + # netcdf4 + # requests +cftime==1.6.5 + # via netcdf4 +charset-normalizer==3.4.7 + # via requests +ckzg==2.1.7 + # via eth-account +click==8.4.1 + # via uvicorn +colorama==0.4.6 ; sys_platform == 'win32' + # via + # click + # loguru +cytoolz==1.1.0 ; implementation_name == 'cpython' + # via eth-utils +eth-abi==5.2.0 + # via + # eth-account + # web3 +eth-account==0.13.7 + # via web3 +eth-hash==0.8.0 + # via + # eth-utils + # web3 +eth-keyfile==0.8.1 + # via eth-account +eth-keys==0.7.0 + # via + # eth-account + # eth-keyfile +eth-rlp==2.2.0 + # via eth-account +eth-typing==6.0.0 + # via + # eth-abi + # eth-keys + # eth-utils + # web3 +eth-utils==6.0.0 + # via + # eth-abi + # eth-account + # eth-keyfile + # eth-keys + # eth-rlp + # rlp + # web3 +fastapi==0.137.0 + # via -r requirements.txt +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal +h11==0.16.0 + # via + # httpcore + # uvicorn +hexbytes==1.3.1 + # via + # eth-account + # eth-rlp + # web3 +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via -r requirements.txt +idna==3.18 + # via + # anyio + # httpx + # requests + # yarl +loguru==0.7.3 + # via -r requirements.txt +multidict==6.7.1 + # via + # aiohttp + # yarl +netcdf4==1.7.4 + # via -r requirements.txt +numpy==2.4.6 + # via + # -r requirements.txt + # cftime + # netcdf4 +parsimonious==0.10.0 + # via eth-abi +propcache==0.5.2 + # via + # aiohttp + # yarl +pyaes==1.6.1 + # via telethon +pyasn1==0.6.3 + # via rsa +pycryptodome==3.23.0 + # via + # eth-hash + # eth-keyfile +pydantic==2.13.4 + # via + # eth-account + # eth-utils + # fastapi + # web3 +pydantic-core==2.46.4 + # via pydantic +pytelegrambotapi==4.34.0 + # via -r requirements.txt +python-dotenv==1.2.2 + # via -r requirements.txt +pyunormalize==17.0.0 + # via web3 +pywin32==312 ; sys_platform == 'win32' + # via web3 +pyyaml==6.0.3 + # via -r requirements.txt +redis==6.4.0 + # via -r requirements.txt +regex==2026.5.9 + # via parsimonious +requests==2.34.2 + # via + # -r requirements.txt + # pytelegrambotapi + # web3 +rlp==4.1.0 + # via + # eth-account + # eth-rlp +rsa==4.9.1 + # via telethon +starlette==1.3.1 + # via fastapi +telethon==1.43.2 + # via -r requirements.txt +toolz==1.1.0 ; implementation_name == 'cpython' or implementation_name == 'pypy' + # via + # cytoolz + # eth-utils +types-requests==2.33.0.20260518 + # via web3 +typing-extensions==4.15.0 + # via + # aiohttp + # aiosignal + # anyio + # eth-typing + # fastapi + # pydantic + # pydantic-core + # starlette + # typing-inspection + # web3 +typing-inspection==0.4.2 + # via + # fastapi + # pydantic +urllib3==2.7.0 + # via + # requests + # types-requests +uvicorn==0.49.0 + # via -r requirements.txt +web3==7.16.0 + # via -r requirements.txt +websockets==15.0.1 + # via + # -r requirements.txt + # web3 +win32-setctime==1.2.0 ; sys_platform == 'win32' + # via loguru +yarl==1.24.2 + # via aiohttp diff --git a/requirements.txt b/requirements.txt index 2cffcb52..64042582 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ -requests -httpx -loguru -pyTelegramBotAPI -telethon -python-dotenv -PyYAML -netCDF4 -numpy -web3 -fastapi -uvicorn -websockets -redis>=5.0.0 +requests>=2.32,<3 +httpx>=0.28,<1 +loguru>=0.7,<1 +pyTelegramBotAPI>=4.23,<5 +telethon>=1.40,<2 +python-dotenv>=1.1,<2 +PyYAML>=6.0,<7 +netCDF4>=1.7,<2 +numpy>=2.2,<3 +web3>=7.13,<8 +fastapi>=0.115,<1 +uvicorn>=0.34,<1 +websockets>=15,<16 +redis>=5.0,<7 diff --git a/scripts/scrub_secrets.py b/scripts/scrub_secrets.py index 519891ab..88a6c052 100644 --- a/scripts/scrub_secrets.py +++ b/scripts/scrub_secrets.py @@ -51,6 +51,7 @@ REPLACE_WITH_EXAMPLE: dict[str, str] = { ".env": ".env.example", ".env.local": ".env.example", "frontend/.env.local": "frontend/.env.example", + "frontend/.env.production": "frontend/.env.example", } diff --git a/tests/test_dependency_and_env_policy.py b/tests/test_dependency_and_env_policy.py new file mode 100644 index 00000000..0f54bdb0 --- /dev/null +++ b/tests/test_dependency_and_env_policy.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] + + +def _meaningful_requirement_lines(path: Path) -> list[str]: + return [ + line.strip() + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + + +def test_python_dependency_builds_use_lock_files() -> None: + assert (ROOT / "requirements.lock").is_file() + assert (ROOT / "requirements-dev.lock").is_file() + + dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") + assert "COPY requirements.lock ." in dockerfile + assert "pip install --prefer-binary -r requirements.lock" in dockerfile + assert "pip install --prefer-binary -r requirements.txt" not in dockerfile + + ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + assert "pip install -r requirements.lock -r requirements-dev.lock" in ci + assert "pip install -r requirements.txt -r requirements-dev.txt" not in ci + + +def test_requirement_input_files_keep_upper_bounds() -> None: + for path in (ROOT / "requirements.txt", ROOT / "requirements-dev.txt"): + for line in _meaningful_requirement_lines(path): + assert "<" in line, f"{path.name} entry lacks an upper bound: {line}" + + +def test_frontend_runtime_env_files_are_not_tracked_or_in_docker_context() -> None: + if shutil.which("git") is None or not (ROOT / ".git").exists(): + pytest.skip("git metadata is required for tracking policy checks") + + tracked = subprocess.run( + [ + "git", + "ls-files", + "--", + "frontend/.env.local", + "frontend/.env.production", + ], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + assert tracked.stdout.strip() == "" + + frontend_gitignore = (ROOT / "frontend" / ".gitignore").read_text( + encoding="utf-8" + ) + assert ".env.local" in frontend_gitignore + assert ".env.production" in frontend_gitignore + + frontend_dockerignore = (ROOT / "frontend" / ".dockerignore").read_text( + encoding="utf-8" + ) + assert ".env" in frontend_dockerignore + assert ".env.*" in frontend_dockerignore + assert "!.env.example" in frontend_dockerignore