diff --git a/server/.env.example b/server/.env.example index 5703501dc..76637a8ef 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,35 +1,7 @@ -# Environment Configuration Example -# Copy this file to .env and update with your own values - -# Application Settings debug=false url_prefix=/api - -# Security Configuration -# Generate with: openssl rand -hex 32 -secret_key=CHANGE_THIS_TO_A_RANDOM_SECRET_KEY_USE_OPENSSL_RAND_HEX_32 - -# Database Configuration -# Use a strong password in production -database_url=postgresql://postgres:CHANGE_THIS_STRONG_PASSWORD@localhost:5432/eigent - -# Docker Compose Database Settings (if using docker-compose) -POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD -POSTGRES_USER=postgres -POSTGRES_DB=eigent - -# JWT Configuration -# Token expiration in seconds (3600 = 1 hour, recommended for production) -JWT_EXPIRATION=3600 - -# Chat Share Security -# Generate with: openssl rand -hex 32 -CHAT_SHARE_SECRET_KEY=CHANGE_THIS_TO_A_RANDOM_SECRET_KEY -# Generate with: openssl rand -hex 16 -CHAT_SHARE_SALT=CHANGE_THIS_TO_A_RANDOM_SALT - -# Stack Auth Configuration (Optional) -# Leave empty if not using Stack Auth -STACK_AUTH_PROJECT_ID= -STACK_AUTH_API_KEY= -STACK_AUTH_BASE_URL= \ No newline at end of file +secret_key=postgres +database_url=postgresql://postgres:postgres@localhost:5432/postgres +# Chat Share Secret Key +CHAT_SHARE_SECRET_KEY=put-your-secret-key-here +CHAT_SHARE_SALT=put-your-encode-salt-here diff --git a/server/README_CN.md b/server/README_CN.md index 6e2204bba..7bd08f671 100644 --- a/server/README_CN.md +++ b/server/README_CN.md @@ -35,6 +35,7 @@ docker compose up -d 2) 启动前端(本地模式) - 在项目根目录创建或修改 `.env.development`,开启本地模式并指向本地后端: ```bash +VITE_BASE_URL=/api VITE_USE_LOCAL_PROXY=true VITE_PROXY_URL=http://localhost:3001 ``` diff --git a/server/README_EN.md b/server/README_EN.md index e6ae3bd43..5c772ef38 100644 --- a/server/README_EN.md +++ b/server/README_EN.md @@ -34,6 +34,7 @@ docker compose up -d 2) Start Frontend (Local Mode) - In the project root directory, create or modify `.env.development` to enable local mode and point to the local backend: ```bash +VITE_BASE_URL=/api VITE_USE_LOCAL_PROXY=true VITE_PROXY_URL=http://localhost:3001 ``` diff --git a/server/alembic/versions/2025_08_11_2107-0001_init_initial_schema.py b/server/alembic/versions/2025_08_11_2107-0001_init_initial_schema.py index e736e214f..31c88c262 100644 --- a/server/alembic/versions/2025_08_11_2107-0001_init_initial_schema.py +++ b/server/alembic/versions/2025_08_11_2107-0001_init_initial_schema.py @@ -52,8 +52,6 @@ def upgrade() -> None: "admin_role", sa.Column("admin_id", sa.Integer(), nullable=False), sa.Column("role_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["admin_id"], ["admin.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["role_id"], ["role.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("admin_id", "role_id"), ) op.create_table( @@ -285,7 +283,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=True), sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column("privacy_setting", sa.JSON(), nullable=True), + sa.Column("pricacy_setting", sa.JSON(), nullable=True), sa.ForeignKeyConstraint( ["user_id"], ["user.id"], diff --git a/server/app/component/auth.py b/server/app/component/auth.py index b751b4378..4a74d410f 100644 --- a/server/app/component/auth.py +++ b/server/app/component/auth.py @@ -39,36 +39,17 @@ class Auth: id = payload["id"] if payload["exp"] < int(datetime.now().timestamp()): raise TokenException(code.token_expired, _("Validate credentials expired")) - # Accept both old tokens (without type) and new tokens (with type) - # Old tokens are treated as access tokens for backward compatibility - token_type = payload.get("type", "access") - if token_type not in ["access", "refresh"]: - raise TokenException(code.token_invalid, _("Invalid token type")) except InvalidTokenError: raise TokenException(code.token_invalid, _("Could not validate credentials")) return Auth(id, payload["exp"]) @classmethod def create_access_token(cls, user_id: int, expires_delta: timedelta | None = None): - to_encode: dict = {"id": user_id, "type": "access"} + to_encode: dict = {"id": user_id} if expires_delta: expire = datetime.now() + expires_delta else: - # Get expiration from environment or default to 1 hour - expiration_seconds = int(env("JWT_EXPIRATION", "3600")) - expire = datetime.now() + timedelta(seconds=expiration_seconds) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, Auth.SECRET_KEY, algorithm="HS256") - return encoded_jwt - - @classmethod - def create_refresh_token(cls, user_id: int, expires_delta: timedelta | None = None): - to_encode: dict = {"id": user_id, "type": "refresh"} - if expires_delta: - expire = datetime.now() + expires_delta - else: - # Refresh tokens last 7 days by default - expire = datetime.now() + timedelta(days=7) + expire = datetime.now() + timedelta(days=30) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, Auth.SECRET_KEY, algorithm="HS256") return encoded_jwt diff --git a/server/app/component/validator/McpServer.py b/server/app/component/validator/McpServer.py index 346d1b6c3..d23beb020 100644 --- a/server/app/component/validator/McpServer.py +++ b/server/app/component/validator/McpServer.py @@ -1,35 +1,11 @@ -from pydantic import BaseModel, ValidationError, field_validator, validator +from pydantic import BaseModel, ValidationError, field_validator from typing import Dict, List, Optional -import re -import os class McpServerItem(BaseModel): command: str args: List[str] env: Optional[Dict[str, str]] = None - - @validator('command') - def validate_command(cls, v): - # Only allow alphanumeric, dash, underscore, forward slash, and dot - if not re.match(r'^[a-zA-Z0-9_\-./]+$', v): - raise ValueError('Command contains invalid characters') - # Prevent directory traversal - if '..' in v: - raise ValueError('Directory traversal not allowed') - # Check if it's an absolute path or a command name - if '/' in v and not os.path.isabs(v): - raise ValueError('Relative paths not allowed') - return v - - @validator('args', each_item=True) - def validate_args(cls, v): - # Prevent shell metacharacters that could lead to command injection - dangerous_chars = ['&', '|', ';', '$', '`', '(', ')', '<', '>', '\n', '\r'] - for char in dangerous_chars: - if char in v: - raise ValueError(f'Argument contains dangerous character: {char}') - return v class McpServersModel(BaseModel): @@ -39,21 +15,6 @@ class McpServersModel(BaseModel): class McpRemoteServer(BaseModel): server_name: str server_url: str - - @validator('server_url') - def validate_server_url(cls, v): - # Only allow http/https URLs - if not v.startswith(('http://', 'https://')): - raise ValueError('Only HTTP/HTTPS URLs are allowed') - # Basic URL validation to prevent SSRF - # In production, you should use a proper URL validation library - # and implement domain allowlisting - forbidden_hosts = ['localhost', '127.0.0.1', '0.0.0.0', '169.254.169.254'] - from urllib.parse import urlparse - parsed = urlparse(v) - if parsed.hostname in forbidden_hosts: - raise ValueError('Access to this host is forbidden') - return v def validate_mcp_servers(data: dict): diff --git a/server/app/controller/chat/step_controller.py b/server/app/controller/chat/step_controller.py index 21a837998..c33639112 100644 --- a/server/app/controller/chat/step_controller.py +++ b/server/app/controller/chat/step_controller.py @@ -67,7 +67,8 @@ async def get_chat_step(step_id: int, session: Session = Depends(session), auth: @router.post("/steps", name="create chat step") -async def create_chat_step(step: ChatStepIn, session: Session = Depends(session), auth: Auth = Depends(auth_must)): +# TODO Limit request sources +async def create_chat_step(step: ChatStepIn, session: Session = Depends(session)): chat_step = ChatStep( task_id=step.task_id, step=step.step, diff --git a/server/app/controller/provider/provider_controller.py b/server/app/controller/provider/provider_controller.py index 0caadb83c..410f246fc 100644 --- a/server/app/controller/provider/provider_controller.py +++ b/server/app/controller/provider/provider_controller.py @@ -63,7 +63,7 @@ async def put(id: int, data: ProviderIn, session: Session = Depends(session), au model.api_key = data.api_key model.endpoint_url = data.endpoint_url model.encrypted_config = data.encrypted_config - model.is_valid = data.is_valid + model.is_vaild = data.is_vaild model.save(session) session.refresh(model) return model diff --git a/server/app/controller/user/login_controller.py b/server/app/controller/user/login_controller.py index ec56fb6eb..908e63da9 100644 --- a/server/app/controller/user/login_controller.py +++ b/server/app/controller/user/login_controller.py @@ -8,20 +8,13 @@ from app.component.encrypt import password_verify from app.component.stack_auth import StackAuth from app.exception.exception import UserException from app.model.user.user import LoginByPasswordIn, LoginResponse, Status, User, RegisterIn -from pydantic import BaseModel from loguru import logger from app.component.environment import env -from datetime import datetime -import jwt router = APIRouter(tags=["Login/Registration"]) -class RefreshTokenRequest(BaseModel): - refresh_token: str - - @router.post("/login", name="login by email or password") async def by_password(data: LoginByPasswordIn, session: Session = Depends(session)) -> LoginResponse: """ @@ -30,11 +23,7 @@ async def by_password(data: LoginByPasswordIn, session: Session = Depends(sessio user = User.by(User.email == data.email, s=session).one_or_none() if not user or not password_verify(data.password, user.password): raise UserException(code.password, _("Account or password error")) - return LoginResponse( - access_token=Auth.create_access_token(user.id), - refresh_token=Auth.create_refresh_token(user.id), - email=user.email - ) + return LoginResponse(token=Auth.create_access_token(user.id), email=user.email) @router.post("/login-by_stack", name="login by stack") @@ -68,11 +57,7 @@ async def by_stack_auth( s.add(user) s.commit() session.refresh(user) - return LoginResponse( - access_token=Auth.create_access_token(user.id), - refresh_token=Auth.create_refresh_token(user.id), - email=user.email - ) + return LoginResponse(token=Auth.create_access_token(user.id), email=user.email) except Exception as e: s.rollback() logger.error(f"Failed to register: {e}") @@ -80,11 +65,7 @@ async def by_stack_auth( else: if user.status == Status.Block: raise UserException(code.error, _("Your account has been blocked.")) - return LoginResponse( - access_token=Auth.create_access_token(user.id), - refresh_token=Auth.create_refresh_token(user.id), - email=user.email - ) + return LoginResponse(token=Auth.create_access_token(user.id), email=user.email) @router.post("/register", name="register by email/password") @@ -107,40 +88,3 @@ async def register(data: RegisterIn, session: Session = Depends(session)): logger.error(f"Failed to register: {e}") raise UserException(code.error, _("Failed to register")) return {"status": "success"} - - -@router.post("/refresh", name="refresh access token") -async def refresh_token(data: RefreshTokenRequest, session: Session = Depends(session)) -> LoginResponse: - """ - Refresh the access token using a valid refresh token. - """ - try: - # Decode the refresh token - payload = jwt.decode(data.refresh_token, Auth.SECRET_KEY, algorithms=["HS256"]) - - # Verify it's a refresh token - if payload.get("type") != "refresh": - raise HTTPException(status_code=401, detail="Invalid token type") - - # Check if expired - if payload["exp"] < int(datetime.now().timestamp()): - raise HTTPException(status_code=401, detail="Refresh token expired") - - # Get the user - user_id = payload["id"] - user = session.get(User, user_id) - if not user: - raise HTTPException(status_code=401, detail="User not found") - - # Check if user is blocked - if user.status == Status.Block: - raise HTTPException(status_code=401, detail="User account is blocked") - - # Generate new tokens - return LoginResponse( - access_token=Auth.create_access_token(user.id), - refresh_token=Auth.create_refresh_token(user.id), - email=user.email - ) - except jwt.InvalidTokenError: - raise HTTPException(status_code=401, detail="Invalid refresh token") diff --git a/server/app/controller/user/user_controller.py b/server/app/controller/user/user_controller.py index 0b5117fef..cd8ecc3b9 100644 --- a/server/app/controller/user/user_controller.py +++ b/server/app/controller/user/user_controller.py @@ -50,7 +50,7 @@ def get_privacy(session: Session = Depends(session), auth: Auth = Depends(auth_m if not model: return UserPrivacySettings.default_settings() - return model.privacy_setting + return model.pricacy_setting @router.put("/user/privacy", name="update user privacy") @@ -61,13 +61,13 @@ def put_privacy(data: UserPrivacySettings, session: Session = Depends(session), default_settings = UserPrivacySettings.default_settings() if model: - model.privacy_setting = {**model.privacy_setting, **data.model_dump()} + model.pricacy_setting = {**model.pricacy_setting, **data.model_dump()} model.save(session) else: - model = UserPrivacy(user_id=user_id, privacy_setting={**default_settings, **data.model_dump()}) + model = UserPrivacy(user_id=user_id, pricacy_setting={**default_settings, **data.model_dump()}) model.save(session) - return model.privacy_setting + return model.pricacy_setting @router.get("/user/current_credits", name="get user current credits") diff --git a/server/app/model/provider/provider.py b/server/app/model/provider/provider.py index a3cb3c567..2a1da3cb5 100644 --- a/server/app/model/provider/provider.py +++ b/server/app/model/provider/provider.py @@ -9,7 +9,7 @@ from sqlalchemy import text from app.model.abstract.model import AbstractModel, DefaultTimes -class ValidStatus(IntEnum): +class VaildStatus(IntEnum): not_valid = 1 is_valid = 2 @@ -23,9 +23,9 @@ class Provider(AbstractModel, DefaultTimes, table=True): endpoint_url: str = "" encrypted_config: dict | None = Field(default=None, sa_column=Column(JSON)) prefer: bool = Field(default=False, sa_column=Column(Boolean, server_default=text("false"))) - is_valid: ValidStatus = Field( - default=ValidStatus.not_valid, - sa_column=Column(ChoiceType(ValidStatus, SmallInteger()), server_default=text("1")), + is_vaild: VaildStatus = Field( + default=VaildStatus.not_valid, + sa_column=Column(ChoiceType(VaildStatus, SmallInteger()), server_default=text("1")), ) @@ -35,7 +35,7 @@ class ProviderIn(BaseModel): api_key: str endpoint_url: str encrypted_config: dict | None = None - is_valid: ValidStatus = ValidStatus.not_valid + is_vaild: VaildStatus = VaildStatus.not_valid prefer: bool = False diff --git a/server/app/model/user/privacy.py b/server/app/model/user/privacy.py index b5fab5743..c749bc526 100644 --- a/server/app/model/user/privacy.py +++ b/server/app/model/user/privacy.py @@ -10,7 +10,7 @@ from app.model.abstract.model import AbstractModel, DefaultTimes class UserPrivacy(AbstractModel, DefaultTimes, table=True): id: int = Field(default=None, primary_key=True) user_id: int = Field(unique=True, foreign_key="user.id") - privacy_setting: dict = Field(default="{}", sa_column=Column(JSON)) + pricacy_setting: dict = Field(default="{}", sa_column=Column(JSON)) class UserPrivacySettings(BaseModel): diff --git a/server/app/model/user/user.py b/server/app/model/user/user.py index 10dafd5aa..3764c31ea 100644 --- a/server/app/model/user/user.py +++ b/server/app/model/user/user.py @@ -43,14 +43,8 @@ class LoginByPasswordIn(BaseModel): class LoginResponse(BaseModel): - access_token: str - refresh_token: str - token_type: str = "Bearer" + token: str email: EmailStr - # Backward compatibility - @property - def token(self) -> str: - return self.access_token class UserIn(BaseModel): diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 3e7552324..124d3d93f 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -7,9 +7,9 @@ services: container_name: eigent_postgres restart: unless-stopped environment: - POSTGRES_DB: ${POSTGRES_DB:-eigent} - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: eigent + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 123456 POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" ports: - "5432:5432" @@ -30,13 +30,13 @@ services: context: . dockerfile: Dockerfile args: - database_url: ${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/eigent} + database_url: postgresql://postgres:123456@postgres:5432/eigent container_name: eigent_api restart: unless-stopped ports: - "3001:5678" environment: - - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/eigent} + - DATABASE_URL=postgresql://postgres:123456@postgres:5432/eigent - ENVIRONMENT=production - DEBUG=false # volumes: diff --git a/server/main.py b/server/main.py index 40b2171a6..e440a7b13 100644 --- a/server/main.py +++ b/server/main.py @@ -3,22 +3,23 @@ from app.component.environment import auto_include_routers, env from loguru import logger import os from fastapi.staticfiles import StaticFiles -from fastapi import status -from fastapi.responses import JSONResponse - -# Health check endpoint -@api.get("/health", tags=["Health"]) -async def health_check(): - """Health check endpoint for monitoring.""" - return JSONResponse( - status_code=status.HTTP_200_OK, - content={"status": "healthy", "service": "eigent-api"} - ) prefix = env("url_prefix", "") auto_include_routers(api, prefix, "app/controller") public_dir = os.environ.get("PUBLIC_DIR") or os.path.join(os.path.dirname(__file__), "app", "public") -api.mount("/public", StaticFiles(directory=public_dir), name="public") +# Ensure static directory exists or gracefully skip mounting +if not os.path.isdir(public_dir): + try: + os.makedirs(public_dir, exist_ok=True) + logger.warning(f"Public directory did not exist. Created: {public_dir}") + except Exception as e: + logger.error(f"Public directory missing and could not be created: {public_dir}. Error: {e}") + public_dir = None + +if public_dir and os.path.isdir(public_dir): + api.mount("/public", StaticFiles(directory=public_dir), name="public") +else: + logger.warning("Skipping /public mount because public directory is unavailable") logger.add( "runtime/log/app.log", diff --git a/server/start.sh b/server/start.sh index 31d4795e4..2bb87620c 100644 --- a/server/start.sh +++ b/server/start.sh @@ -1,16 +1,16 @@ #!/bin/bash -# 等待数据库启动 +# wait for database to be ready echo "Waiting for database to be ready..." while ! nc -z postgres 5432; do sleep 1 done echo "Database is ready!" -# 运行数据库迁移 +# run database migrations echo "Running database migrations..." uv run alembic upgrade head -# 启动应用 +# start application echo "Starting application..." exec uv run uvicorn main:api --host 0.0.0.0 --port 5678 \ No newline at end of file