From 1e8b20e30b1258d1cd3ebf3af5c6212da4b84b48 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Mon, 4 May 2026 09:33:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=BB=95=E8=BF=87=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs GHSA-6959-99pq-c56x --- .env.template | 5 + backend/server/utils/auth_middleware.py | 16 ++- backend/server/utils/auth_utils.py | 58 ++++++++-- .../test/integration/api/test_auth_router.py | 25 ++++- backend/test/unit/test_auth_utils.py | 106 +++++++++++++++++- docker-compose.prod.yml | 1 + docker-compose.yml | 1 + docs/develop-guides/roadmap.md | 1 + scripts/init.ps1 | 60 ++++++++++ scripts/init.sh | 59 ++++++++++ 10 files changed, 318 insertions(+), 14 deletions(-) diff --git a/.env.template b/.env.template index 08661e08..f07fad45 100644 --- a/.env.template +++ b/.env.template @@ -5,6 +5,11 @@ SAVE_DIR=./saves SILICONFLOW_API_KEY= # 推荐使用硅基流动免费服务 https://cloud.siliconflow.cn/i/Eo5yTHGJ TAVILY_API_KEY= # 获取搜索服务的 api key 请访问 https://app.tavily.com/ +# JWT 安全配置:首次部署请生成随机值,初始化脚本会在留空时自动生成 +YUXI_ENV=development +JWT_SECRET_KEY= +YUXI_INSTANCE_ID= + # # 其余可选配置 # OPENAI_API_KEY= # OPENAI_API_BASE= diff --git a/backend/server/utils/auth_middleware.py b/backend/server/utils/auth_middleware.py index 200a9176..15f891de 100644 --- a/backend/server/utils/auth_middleware.py +++ b/backend/server/utils/auth_middleware.py @@ -4,14 +4,14 @@ import re from fastapi import Depends, Header, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select - +from sqlalchemy.ext.asyncio import AsyncSession from yuxi.storage.postgres.manager import pg_manager -from yuxi.storage.postgres.models_business import User, APIKey -from server.utils.auth_utils import AuthUtils +from yuxi.storage.postgres.models_business import APIKey, User from yuxi.utils.datetime_utils import utc_now_naive +from server.utils.auth_utils import AuthUtils + # 定义OAuth2密码承载器,指定token URL oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False) @@ -115,10 +115,16 @@ async def get_current_user( headers={"WWW-Authenticate": "Bearer"}, ) - result = await db.execute(select(User).filter(User.id == int(user_id))) + result = await db.execute(select(User).filter(User.id == int(user_id), User.is_deleted == 0)) user = result.scalar_one_or_none() if user is None: raise credentials_exception + if user.is_login_locked(): + raise HTTPException( + status_code=status.HTTP_423_LOCKED, + detail="登录被锁定,请稍后重试", + headers={"X-Lock-Remaining": str(user.get_remaining_lock_time())}, + ) return user diff --git a/backend/server/utils/auth_utils.py b/backend/server/utils/auth_utils.py index 1a65b256..87621c3d 100644 --- a/backend/server/utils/auth_utils.py +++ b/backend/server/utils/auth_utils.py @@ -1,22 +1,52 @@ import hashlib import hmac import os +import secrets from datetime import timedelta from typing import Any import jwt from argon2 import PasswordHasher from argon2.exceptions import InvalidHash, VerificationError, VerifyMismatchError - from yuxi.utils.datetime_utils import utc_now # JWT配置 -JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "yuxi_know_secure_key") +LEGACY_JWT_SECRET_KEY = "yuxi_know_secure_key" JWT_ALGORITHM = "HS256" JWT_EXPIRATION = 7 * 24 * 60 * 60 # 7天过期 +JWT_AUDIENCE = "yuxi-know-api" PASSWORD_HASHER = PasswordHasher() +def _is_production_env() -> bool: + return os.environ.get("YUXI_ENV", "development").strip().lower() in {"prod", "production"} + + +def _get_or_create_dev_env(name: str, value_factory) -> str: + value = os.environ.get(name, "").strip() + if value: + return value + if _is_production_env(): + raise ValueError(f"{name} 未配置,请在生产环境的 .env.prod 中设置持久化随机值") + + value = value_factory() + os.environ[name] = value + print(f"{name} 未配置,开发环境已自动生成临时随机值,服务重启后会重新生成。") + return value + + +def _get_jwt_secret_key() -> str: + secret_key = _get_or_create_dev_env("JWT_SECRET_KEY", lambda: secrets.token_hex(32)) + if secret_key == LEGACY_JWT_SECRET_KEY: + raise ValueError("JWT_SECRET_KEY 不能使用历史默认密钥,请重新生成随机强密钥") + return secret_key + + +def _get_jwt_issuer() -> str: + instance_id = _get_or_create_dev_env("YUXI_INSTANCE_ID", lambda: f"instance-{secrets.token_hex(8)}") + return f"yuxi-know:{instance_id}" + + class AuthUtils: """认证工具类""" @@ -53,26 +83,40 @@ class AuthUtils: else: expire = utc_now() + timedelta(seconds=JWT_EXPIRATION) - to_encode.update({"exp": expire}) + to_encode.update({"exp": expire, "iss": _get_jwt_issuer(), "aud": JWT_AUDIENCE}) # 编码JWT - encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + encoded_jwt = jwt.encode(to_encode, _get_jwt_secret_key(), algorithm=JWT_ALGORITHM) return encoded_jwt @staticmethod def decode_token(token: str) -> dict[str, Any] | None: """解码验证JWT令牌""" try: - payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) + payload = jwt.decode( + token, + _get_jwt_secret_key(), + algorithms=[JWT_ALGORITHM], + issuer=_get_jwt_issuer(), + audience=JWT_AUDIENCE, + options={"require": ["exp", "sub", "iss", "aud"]}, + ) return payload - except jwt.PyJWTError: + except (jwt.PyJWTError, ValueError): return None @staticmethod def verify_access_token(token: str) -> dict[str, Any]: """验证访问令牌,如果无效则抛出异常""" try: - payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) + payload = jwt.decode( + token, + _get_jwt_secret_key(), + algorithms=[JWT_ALGORITHM], + issuer=_get_jwt_issuer(), + audience=JWT_AUDIENCE, + options={"require": ["exp", "sub", "iss", "aud"]}, + ) return payload except jwt.ExpiredSignatureError: raise ValueError("令牌已过期") diff --git a/backend/test/integration/api/test_auth_router.py b/backend/test/integration/api/test_auth_router.py index ff5b64a5..880ee05c 100644 --- a/backend/test/integration/api/test_auth_router.py +++ b/backend/test/integration/api/test_auth_router.py @@ -25,7 +25,9 @@ async def test_user_is_locked_after_repeated_failed_logins(test_client, standard assert response.status_code == 401, response.text assert response.json()["detail"] == "用户名或密码错误" - locked_response = await test_client.post("/api/auth/token", data={"username": user_id, "password": "wrong-password"}) + locked_response = await test_client.post( + "/api/auth/token", data={"username": user_id, "password": "wrong-password"} + ) assert locked_response.status_code == 423, locked_response.text assert "X-Lock-Remaining" in locked_response.headers assert "账户已被锁定" in locked_response.json()["detail"] @@ -73,3 +75,24 @@ async def test_invalid_token_is_rejected(test_client): headers = {"Authorization": "Bearer not-a-real-token"} response = await test_client.get("/api/auth/me", headers=headers) assert response.status_code == 401 + + +async def test_deleted_user_token_is_rejected(test_client, admin_headers, standard_user): + user_id = standard_user["user"]["id"] + + delete_response = await test_client.delete(f"/api/auth/users/{user_id}", headers=admin_headers) + assert delete_response.status_code == 200, delete_response.text + + profile_response = await test_client.get("/api/auth/me", headers=standard_user["headers"]) + assert profile_response.status_code == 401 + + +async def test_locked_user_token_is_rejected(test_client, standard_user): + user_id = standard_user["user"]["user_id"] + + for _ in range(5): + await test_client.post("/api/auth/token", data={"username": user_id, "password": "wrong-password"}) + + profile_response = await test_client.get("/api/auth/me", headers=standard_user["headers"]) + assert profile_response.status_code == 423 + assert "X-Lock-Remaining" in profile_response.headers diff --git a/backend/test/unit/test_auth_utils.py b/backend/test/unit/test_auth_utils.py index 267f9860..c5957c55 100644 --- a/backend/test/unit/test_auth_utils.py +++ b/backend/test/unit/test_auth_utils.py @@ -1,8 +1,14 @@ from __future__ import annotations import hashlib +import os +from datetime import timedelta -from server.utils.auth_utils import AuthUtils +import jwt +import pytest +from yuxi.utils.datetime_utils import utc_now + +from server.utils.auth_utils import JWT_ALGORITHM, JWT_AUDIENCE, LEGACY_JWT_SECRET_KEY, AuthUtils def test_hash_password_uses_argon2(): @@ -18,3 +24,101 @@ def test_verify_password_accepts_legacy_sha256_format(): assert AuthUtils.verify_password(f"{legacy_hash}:salt", "secret-password") is True assert AuthUtils.verify_password(f"{legacy_hash}:salt", "wrong-password") is False + + +def test_access_token_contains_instance_claims(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness") + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + + token = AuthUtils.create_access_token({"sub": "1"}) + payload = AuthUtils.verify_access_token(token) + + assert payload["sub"] == "1" + assert payload["iss"] == "yuxi-know:pytest-instance" + assert payload["aud"] == JWT_AUDIENCE + + +def test_access_token_auto_generates_dev_secret(monkeypatch): + monkeypatch.setenv("YUXI_ENV", "development") + monkeypatch.delenv("JWT_SECRET_KEY", raising=False) + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + + token = AuthUtils.create_access_token({"sub": "1"}) + + assert AuthUtils.verify_access_token(token)["sub"] == "1" + assert len(os.environ["JWT_SECRET_KEY"]) == 64 + + +def test_access_token_requires_configured_secret_in_production(monkeypatch): + monkeypatch.setenv("YUXI_ENV", "production") + monkeypatch.delenv("JWT_SECRET_KEY", raising=False) + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + + with pytest.raises(ValueError, match="JWT_SECRET_KEY"): + AuthUtils.create_access_token({"sub": "1"}) + + +def test_access_token_requires_non_default_secret(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", LEGACY_JWT_SECRET_KEY) + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + + with pytest.raises(ValueError, match="历史默认密钥"): + AuthUtils.create_access_token({"sub": "1"}) + + +def test_access_token_auto_generates_dev_instance_id(monkeypatch): + monkeypatch.setenv("YUXI_ENV", "development") + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness") + monkeypatch.delenv("YUXI_INSTANCE_ID", raising=False) + + token = AuthUtils.create_access_token({"sub": "1"}) + + assert AuthUtils.verify_access_token(token)["iss"].startswith("yuxi-know:instance-") + assert os.environ["YUXI_INSTANCE_ID"].startswith("instance-") + + +def test_access_token_requires_instance_id_in_production(monkeypatch): + monkeypatch.setenv("YUXI_ENV", "production") + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness") + monkeypatch.delenv("YUXI_INSTANCE_ID", raising=False) + + with pytest.raises(ValueError, match="YUXI_INSTANCE_ID"): + AuthUtils.create_access_token({"sub": "1"}) + + +def test_verify_access_token_rejects_wrong_issuer(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness") + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + token = jwt.encode( + {"sub": "1", "exp": utc_now() + timedelta(minutes=5), "iss": "yuxi-know:other", "aud": JWT_AUDIENCE}, + "test-secret-key-with-enough-randomness", + algorithm=JWT_ALGORITHM, + ) + + assert AuthUtils.decode_token(token) is None + + +def test_verify_access_token_rejects_wrong_audience(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness") + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + token = jwt.encode( + {"sub": "1", "exp": utc_now() + timedelta(minutes=5), "iss": "yuxi-know:pytest-instance", "aud": "other-api"}, + "test-secret-key-with-enough-randomness", + algorithm=JWT_ALGORITHM, + ) + + with pytest.raises(ValueError, match="无效的令牌"): + AuthUtils.verify_access_token(token) + + +def test_verify_access_token_requires_claims(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness") + monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance") + token = jwt.encode( + {"sub": "1", "exp": utc_now() + timedelta(minutes=5)}, + "test-secret-key-with-enough-randomness", + algorithm=JWT_ALGORITHM, + ) + + with pytest.raises(ValueError, match="无效的令牌"): + AuthUtils.verify_access_token(token) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e5efe4cb..e8f6929c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,6 +2,7 @@ x-api-worker-env: &api-worker-env HOST_IP: ${HOST_IP:-} LITE_MODE: ${LITE_MODE:-} RUNNING_IN_DOCKER: "true" + YUXI_ENV: production MODEL_DIR_IN_DOCKER: /models PYTHONPATH: /app/package POSTGRES_URL: ${POSTGRES_URL:-postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-yuxi_know}} diff --git a/docker-compose.yml b/docker-compose.yml index cd4c8522..d9b14589 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ x-api-worker-env: &api-worker-env HOST_IP: ${HOST_IP:-} LITE_MODE: ${LITE_MODE:-false} RUNNING_IN_DOCKER: "true" + YUXI_ENV: development MODEL_DIR_IN_DOCKER: /models # DBs and other services POSTGRES_URL: ${POSTGRES_URL:-postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-yuxi_know}} diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index a969a546..063cb249 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -35,6 +35,7 @@ ### 0.6.2 开发记录 +- 加固 JWT 鉴权安全:移除历史默认密钥回退,初始化脚本支持生成并持久化 `JWT_SECRET_KEY` 与 `YUXI_INSTANCE_ID`,签发和验证令牌时校验 `iss/aud`,并在鉴权阶段拒绝已删除或登录锁定用户继续使用旧令牌访问系统。 - 扩展管理界面交互逻辑重构:将 MCP / Subagents / Skills 三个标签页从「左侧边栏 + 右侧详情面板」布局重构为「卡片式网格布局 + 路由跳转二级页面」布局,工具标签页改为卡片网格布局 + 弹窗详情(保持弹窗内容不变)。新增共享组件 `ExtensionCard`、`ExtensionCardGrid`、`ExtensionToolbar`、`ExtensionDetailLayout`,详情页(`McpDetailView`、`SubagentDetailView`、`SkillDetailView`)使用居中宽度限制,路由规划为 `/extensions/mcp/:name`、`/extensions/subagent/:name`、`/extensions/skill/:slug`。 - 统一卡片样式:`ExtensionCard` 新增 `tags` prop 支持传入 `[{label, color}]` 数组,内部使用 `` 渲染,与知识库卡片标签风格统一;知识库列表页 `DataBaseView` 改用 `ExtensionCard` + `ExtensionCardGrid` 替代原有自定义卡片,移除冗余 card 样式。 - 调整应用主导航:`AppLayout` 从默认窄栏升级为默认展开的侧边栏,保留折叠态图标导航;侧边栏样式收敛为 14px 文本 + 18px 图标的标准紧凑密度,并统一导航项、任务中心、GitHub、用户信息的图标与文字对齐。折叠态改为仅通过显式按钮展开,避免空白区域误触发。 diff --git a/scripts/init.ps1 b/scripts/init.ps1 index 6e267f79..0514065f 100644 --- a/scripts/init.ps1 +++ b/scripts/init.ps1 @@ -2,12 +2,49 @@ # This script helps set up the environment for the Yuxi project # Note: API keys will be visible during input - use with care +function New-RandomHex($ByteCount) { + $bytes = [byte[]]::new($ByteCount) + [System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes) + return -join ($bytes | ForEach-Object { $_.ToString("x2") }) +} + +function Test-EnvValue($Name) { + return [bool](Select-String -Path ".env" -Pattern "^$Name=.+" -Quiet) +} + +function Ensure-JwtEnv { + if ((Test-EnvValue "JWT_SECRET_KEY") -and (Test-EnvValue "YUXI_INSTANCE_ID")) { + return + } + + Write-Host "JWT security settings are missing in .env." -ForegroundColor Yellow + $JWT_SECRET_KEY = Read-Host "Please enter your JWT_SECRET_KEY (press Enter to auto-generate)" + if ([string]::IsNullOrEmpty($JWT_SECRET_KEY)) { + $JWT_SECRET_KEY = New-RandomHex 32 + Write-Host "Generated JWT_SECRET_KEY and saved it to .env." -ForegroundColor Green + } + + $YUXI_INSTANCE_ID = Read-Host "Please enter your YUXI_INSTANCE_ID (press Enter to auto-generate)" + if ([string]::IsNullOrEmpty($YUXI_INSTANCE_ID)) { + $YUXI_INSTANCE_ID = "instance-$(New-RandomHex 8)" + Write-Host "Generated YUXI_INSTANCE_ID and saved it to .env." -ForegroundColor Green + } + + @" + +# JWT security settings +JWT_SECRET_KEY=$JWT_SECRET_KEY +YUXI_INSTANCE_ID=$YUXI_INSTANCE_ID +"@ | Add-Content -Path ".env" -Encoding UTF8 +} + Write-Host "🚀 Initializing Yuxi project..." -ForegroundColor Cyan Write-Host "==================================" -ForegroundColor Cyan # Check if .env file exists if (Test-Path ".env") { Write-Host "✅ .env file already exists. Skipping environment setup." -ForegroundColor Green + Ensure-JwtEnv } else { Write-Host "📝 .env file not found. Let's set up your environment variables." -ForegroundColor Yellow Write-Host "" @@ -32,6 +69,20 @@ if (Test-Path ".env") { $TAVILY_API_KEY = Read-Host "Please enter your TAVILY_API_KEY (press Enter to skip)" + Write-Host "" + Write-Host "JWT security settings" -ForegroundColor Yellow + $JWT_SECRET_KEY = Read-Host "Please enter your JWT_SECRET_KEY (press Enter to auto-generate)" + if ([string]::IsNullOrEmpty($JWT_SECRET_KEY)) { + $JWT_SECRET_KEY = New-RandomHex 32 + Write-Host "Generated JWT_SECRET_KEY and saved it to .env." -ForegroundColor Green + } + + $YUXI_INSTANCE_ID = Read-Host "Please enter your YUXI_INSTANCE_ID (press Enter to auto-generate)" + if ([string]::IsNullOrEmpty($YUXI_INSTANCE_ID)) { + $YUXI_INSTANCE_ID = "instance-$(New-RandomHex 8)" + Write-Host "Generated YUXI_INSTANCE_ID and saved it to .env." -ForegroundColor Green + } + # Create .env file $envContent = @" # SiliconFlow API Key (required) @@ -44,12 +95,21 @@ SILICONFLOW_API_KEY=$apiKey $envContent += "`nTAVILY_API_KEY=$TAVILY_API_KEY" } + $envContent += @" + +# JWT security settings +JWT_SECRET_KEY=$JWT_SECRET_KEY +YUXI_INSTANCE_ID=$YUXI_INSTANCE_ID +"@ + $envContent | Out-File -FilePath ".env" -Encoding UTF8 Write-Host "✅ .env file created successfully!" -ForegroundColor Green # Clear the variables from memory Remove-Variable -Name "apiKey" -ErrorAction SilentlyContinue Remove-Variable -Name "TAVILY_API_KEY" -ErrorAction SilentlyContinue + Remove-Variable -Name "JWT_SECRET_KEY" -ErrorAction SilentlyContinue + Remove-Variable -Name "YUXI_INSTANCE_ID" -ErrorAction SilentlyContinue } Write-Host "" diff --git a/scripts/init.sh b/scripts/init.sh index 9a5dfe1a..25fce468 100644 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -5,12 +5,49 @@ set -e +generate_hex() { + local length="$1" + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex "$length" + else + tr -dc 'a-f0-9' < /dev/urandom | head -c $((length * 2)) + fi +} + +ensure_jwt_env() { + if grep -Eq '^JWT_SECRET_KEY=.+' .env && grep -Eq '^YUXI_INSTANCE_ID=.+' .env; then + return + fi + + echo "JWT security settings are missing in .env." + read -s -p "Please enter your JWT_SECRET_KEY (press Enter to auto-generate): " JWT_SECRET_KEY + echo "" + if [ -z "$JWT_SECRET_KEY" ]; then + JWT_SECRET_KEY=$(generate_hex 32) + echo "Generated JWT_SECRET_KEY and saved it to .env." + fi + + read -p "Please enter your YUXI_INSTANCE_ID (press Enter to auto-generate): " YUXI_INSTANCE_ID + if [ -z "$YUXI_INSTANCE_ID" ]; then + YUXI_INSTANCE_ID="instance-$(generate_hex 8)" + echo "Generated YUXI_INSTANCE_ID and saved it to .env." + fi + + cat >> .env << EOF + +# JWT security settings +JWT_SECRET_KEY=${JWT_SECRET_KEY} +YUXI_INSTANCE_ID=${YUXI_INSTANCE_ID} +EOF +} + echo "🚀 Initializing Yuxi project..." echo "==================================" # Check if .env file exists if [ -f ".env" ]; then echo "✅ .env file already exists. Skipping environment setup." + ensure_jwt_env else echo "📝 .env file not found. Let's set up your environment variables." echo "" @@ -34,6 +71,21 @@ else echo "Get your API key from: https://app.tavily.com/" read -p "Please enter your TAVILY_API_KEY (press Enter to skip): " TAVILY_API_KEY + echo "" + echo "JWT security settings" + read -s -p "Please enter your JWT_SECRET_KEY (press Enter to auto-generate): " JWT_SECRET_KEY + echo "" + if [ -z "$JWT_SECRET_KEY" ]; then + JWT_SECRET_KEY=$(generate_hex 32) + echo "Generated JWT_SECRET_KEY and saved it to .env." + fi + + read -p "Please enter your YUXI_INSTANCE_ID (press Enter to auto-generate): " YUXI_INSTANCE_ID + if [ -z "$YUXI_INSTANCE_ID" ]; then + YUXI_INSTANCE_ID="instance-$(generate_hex 8)" + echo "Generated YUXI_INSTANCE_ID and saved it to .env." + fi + # Create .env file cat > .env << EOF # SiliconFlow API Key (required) @@ -46,6 +98,13 @@ EOF echo "TAVILY_API_KEY=${TAVILY_API_KEY}" >> .env fi + cat >> .env << EOF + +# JWT security settings +JWT_SECRET_KEY=${JWT_SECRET_KEY} +YUXI_INSTANCE_ID=${YUXI_INSTANCE_ID} +EOF + echo "✅ .env file created successfully!" fi