diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 53be80a..5e29647 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -13,6 +13,7 @@ class Settings(BaseSettings): SECRET_KEY: str = "dev-secret-key-change-in-production" ALGORITHM: str = "HS256" + CORS_ORIGINS: Optional[str] = None # comma-separated extra allowed origins ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 AI_PROVIDER: str = "openai" diff --git a/backend/app/core/security.py b/backend/app/core/security.py index a39de30..534bc76 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -1,21 +1,34 @@ from datetime import datetime, timedelta from typing import Optional, Any +import bcrypt from jose import JWTError, jwt -from passlib.context import CryptContext from fastapi import HTTPException, status, Depends from fastapi.security import OAuth2PasswordBearer from app.core.config import settings -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login") +# We call bcrypt directly rather than through passlib: passlib 1.7.x is +# incompatible with bcrypt >= 4.1 (its version shim raises on the modern +# library). bcrypt only uses the first 72 bytes of a password, so we truncate +# to that to avoid the ValueError bcrypt 5.x raises on longer inputs. Existing +# $2b$ hashes (created via passlib's bcrypt backend) verify unchanged. +_BCRYPT_MAX_BYTES = 72 + + +def _to_bytes(password: str) -> bytes: + return password.encode("utf-8")[:_BCRYPT_MAX_BYTES] + def verify_password(plain: str, hashed: str) -> bool: - return pwd_context.verify(plain, hashed) + try: + return bcrypt.checkpw(_to_bytes(plain), hashed.encode("utf-8")) + except (ValueError, TypeError): + return False def hash_password(password: str) -> str: - return pwd_context.hash(password) + return bcrypt.hashpw(_to_bytes(password), bcrypt.gensalt()).decode("utf-8") def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: diff --git a/backend/app/main.py b/backend/app/main.py index 3e7bf6d..112326a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -22,9 +22,16 @@ app = FastAPI( lifespan=lifespan, ) +# The app is normally served same-origin (nginx proxies /api to the backend), +# so CORS is not exercised in the primary flow. This allowlist exists for direct +# browser access to :8000 during development and for any explicitly configured +# origins. Extra origins can be added via the CORS_ORIGINS env var (comma-separated). +_extra_origins = [o.strip() for o in (settings.CORS_ORIGINS or "").split(",") if o.strip()] app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3000", "http://frontend:3000"], + allow_origins=["http://localhost:3000", "http://frontend:3000", *_extra_origins], + # Also allow localhost and private-network hosts on any port (dev convenience). + allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)(:\d+)?$", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 9873e47..6b06ef8 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -5,7 +5,11 @@ const nextConfig: NextConfig = { return [ { source: "/api/:path*", - destination: `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/api/:path*`, + // Server-side proxy for direct :3000 access. Uses the internal service + // hostname inside Docker (API_INTERNAL_URL=http://backend:8000); falls + // back to localhost for non-container dev. Not used when served via + // nginx, which proxies /api itself. + destination: `${process.env.API_INTERNAL_URL || "http://localhost:8000"}/api/:path*`, }, ]; }, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 970bbe2..c01f73c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,4 +1,8 @@ -const BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; +// Empty string = same-origin. API calls go to /api/... on whatever host served +// the page, and are proxied to the backend by nginx (port 80) or the Next.js +// rewrite in next.config.ts (port 3000). This keeps everything same-origin so +// it works via localhost, the LAN IP, and the Cloudflare tunnel with no CORS. +const BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; let authToken: string | null = null; diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index a700ebd..9754792 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -43,7 +43,10 @@ services: dockerfile: ../infra/Dockerfile.frontend container_name: trustos_frontend environment: - NEXT_PUBLIC_API_URL: http://localhost:8000 + # Empty = same-origin API calls (proxied to backend by nginx / Next rewrite). + # Works via localhost, LAN IP, and Cloudflare tunnel without CORS. + NEXT_PUBLIC_API_URL: "" + API_INTERNAL_URL: "http://backend:8000" ports: - "3000:3000" volumes: