diff --git a/.env.private b/.env.private new file mode 100644 index 000000000..3295a8a10 --- /dev/null +++ b/.env.private @@ -0,0 +1,26 @@ +# ============================================================================= +# PRIVATE API CONFIGURATION (Recommended for production) +# ============================================================================= +# In this mode, the backend is NOT accessible from the Internet. +# The frontend uses API Routes as an internal proxy. +# Users NEVER see /api in URLs, everything is transparent. + +# API Mode +USE_PRIVATE_API=true + +# Backend (only accessible internally) +BACKEND_PORT_MAPPING= +CORS_ORIGINS=http://bracket-frontend:3000 +INTERNAL_API_URL=http://bracket-backend:8400 + +# Frontend - User only sees yourdomain.com +FRONTEND_PORT_MAPPING=172.16.0.4:3000:3000 +# This variable is NOT used in private mode, just for reference +PUBLIC_API_URL=https://yourdomain.com + +# General configuration +JWT_SECRET=change_me_in_production +ADMIN_EMAIL=admin@yourdomain.com +ADMIN_PASSWORD=change_me_in_production +BASE_URL=https://yourdomain.com +HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 \ No newline at end of file diff --git a/.env.public b/.env.public new file mode 100644 index 000000000..d753d887b --- /dev/null +++ b/.env.public @@ -0,0 +1,24 @@ +# ============================================================================= +# PUBLIC API CONFIGURATION +# ============================================================================= +# In this mode, both frontend and backend are accessible from the Internet. +# The browser connects directly to the backend. + +# API Mode +USE_PRIVATE_API=false + +# Backend (publicly accessible) +BACKEND_PORT_MAPPING=172.16.0.4:8400:8400 +CORS_ORIGINS=https://yourdomain.com +INTERNAL_API_URL=http://bracket-backend:8400 + +# Frontend +FRONTEND_PORT_MAPPING=172.16.0.4:3000:3000 +PUBLIC_API_URL=https://api.yourdomain.com + +# General configuration +JWT_SECRET=change_me_in_production +ADMIN_EMAIL=admin@yourdomain.com +ADMIN_PASSWORD=change_me_in_production +BASE_URL=https://api.yourdomain.com +HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 \ No newline at end of file diff --git a/API-MODES.md b/API-MODES.md new file mode 100644 index 000000000..4ebe7fe92 --- /dev/null +++ b/API-MODES.md @@ -0,0 +1,225 @@ +# API modes: Private vs Public + +Overview +- The application supports two modes of API access. +- Private mode (recommended): only the frontend is public; the backend stays private and is reached via the frontend’s internal proxy. +- Public mode: both frontend and backend are public; the frontend calls the backend directly. + +# Private mode (recommended) +## What it is + - The backend is not exposed to the internet. + - The frontend exposes an internal proxy under /api that forwards requests to the backend inside the Docker network. + - This eliminates CORS issues and reduces the attack surface. +## How it works + - Browser → Frontend (public) → /api/... → Frontend proxy → Backend (private) → Response → Browser. +## Configure + - Frontend + - NEXT_PUBLIC_USE_PRIVATE_API = "true" (string) + - INTERNAL_API_BASE_URL points to the backend service URL on the Docker network (for example: http://bracket-backend:8400) + - Expose only the frontend port publicly (for example: 3000) + - Backend + - Do not publish the backend port; the backend must be reachable only from the Docker network + - CORS_ORIGINS must include the frontend container origin (for example: http://bracket-frontend:3000) + - BASE_URL is your public site URL (for example: https://yourdomain.com) +## When to use + - Production behind Cloudflare/Nginx + - Single-frontend deployments where the backend should not be directly reachable + +## Example Docker-Compose in Private Mode +```bash +networks: + bracket_lan: + driver: bridge + +volumes: + bracket_pg_data: + +services: + bracket-backend: + container_name: bracket-backend + depends_on: + - postgres + environment: + ENVIRONMENT: PRODUCTION + # CORS - Internal communication between frontend and backend + CORS_ORIGINS: http://bracket-frontend:3000 + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://youdomain.com + image: ghcr.io/evroon/bracket-backend + networks: + - bracket_lan + # Backend is PRIVATE - Uncomment ports below for PUBLIC API mode + # ports: + # - "8400:8400" + restart: unless-stopped + volumes: + - ./backend/static:/app/static + + bracket-frontend: + container_name: bracket-frontend + environment: + # Private API mode - Uses internal proxy to communicate with backend + NEXT_PUBLIC_USE_PRIVATE_API: "true" + # Backend URL (for proxy reference) + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 + # Internal URL for proxy (server-side only) + INTERNAL_API_BASE_URL: http://bracket-backend:8400 + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + # Use local image with internal proxy + image: ghcr.io/evroon/bracket-frontend + networks: + - bracket_lan + ports: + - "3000:3000" + restart: unless-stopped + + postgres: + environment: + POSTGRES_DB: bracket_prod + POSTGRES_PASSWORD: bracket_prod + POSTGRES_USER: bracket_prod + image: postgres + networks: + - bracket_lan + restart: always + volumes: + - bracket_pg_data:/var/lib/postgresql + +``` + + +# Public mode (direct API) +## What it is + - Both frontend and backend are publicly accessible. + - The frontend calls the backend directly using the public API URL. +## Configure + - Frontend + - NEXT_PUBLIC_USE_PRIVATE_API = "false" (string) + - NEXT_PUBLIC_API_BASE_URL is the public API URL (for example: https://api.yourdomain.com) + - Backend + - Publish the backend port (for example: 8400) + - CORS_ORIGINS must include the public frontend domain (for example: https://yourdomain.com) + - BASE_URL is the public API base URL (for example: https://api.yourdomain.com) +## When to use + - Multi-client scenarios (mobile + web) or when third parties must call your API directly + +## Example Docker-Compose Direct Api +```bash +networks: + bracket_lan: + driver: bridge + +volumes: + bracket_pg_data: + +services: + bracket-backend: + container_name: bracket-backend + depends_on: + - postgres + environment: + # Private API mode - Uses internal proxy to communicate with backend + ENVIRONMENT: PRODUCTION + # CORS - Internal communication between frontend and backend + CORS_ORIGINS: https://youdomain.com + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://youdomain.com + image: ghcr.io/evroon/bracket-backend + networks: + - bracket_lan + # Backend for PUBLIC API mode + ports: + - "8400:8400" + restart: unless-stopped + volumes: + - ./backend/static:/app/static + + bracket-frontend: + container_name: bracket-frontend + environment: + # Private API mode - Uses internal proxy to communicate with backend + NEXT_PUBLIC_USE_PRIVATE_API: "false" + # Backend URL (for proxy reference) + NEXT_PUBLIC_API_BASE_URL: https://api.youdomain.com + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + # Use local image with internal proxy + image: ghcr.io/evroon/bracket-frontend + networks: + - bracket_lan + ports: + - "3000:3000" + restart: unless-stopped + + postgres: + environment: + POSTGRES_DB: bracket_prod + POSTGRES_PASSWORD: bracket_prod + POSTGRES_USER: bracket_prod + image: postgres + networks: + - bracket_lan + restart: always + volumes: + - bracket_pg_data:/var/lib/postgresql + +``` + +# Environment variables +- Frontend + - NEXT_PUBLIC_USE_PRIVATE_API: "true" or "false" to select Private or Public mode + - NEXT_PUBLIC_API_BASE_URL: public backend URL used by the browser in Public mode + - INTERNAL_API_BASE_URL: internal backend URL used by the proxy in Private mode + - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: hCaptcha site key used by forms +- Backend + - ENVIRONMENT: typically PRODUCTION + - CORS_ORIGINS: comma-separated list of allowed origins + - PG_DSN: PostgreSQL connection string + - JWT_SECRET: secret for signing tokens + - ADMIN_EMAIL and ADMIN_PASSWORD: initial admin user + - BASE_URL: public base URL of the app (Private mode: site URL; Public mode: API URL) +- Important + - Variables prefixed with NEXT_PUBLIC_ are embedded at build time in Next.js. Changing them requires rebuilding the frontend image and restarting the container. + - Backend variable changes usually require only a container restart. + +# Docker Compose hints +- Private mode + - Publish only the frontend service port. + - Do not publish the backend port; both services must share the same private Docker network. + - Set NEXT_PUBLIC_USE_PRIVATE_API to "true" and INTERNAL_API_BASE_URL to the backend service URL. + - Set backend CORS_ORIGINS to the frontend container origin (for example: http://bracket-frontend:3000). +- Public mode + - Publish both frontend and backend ports. + - Set NEXT_PUBLIC_USE_PRIVATE_API to "false" and NEXT_PUBLIC_API_BASE_URL to the public API URL. + - Set backend CORS_ORIGINS to include the public frontend domain. + +# Reverse proxy notes +- Private mode + - Only proxy the frontend (for example: Cloudflare/Nginx → frontend:3000). + - Do not expose the backend externally. +- Public mode + - Proxy both frontend and backend using separate hostnames (for example: yourdomain.com → frontend, api.yourdomain.com → backend). + +# Troubleshooting +- Browser shows requests to bracket-backend:8400 or net::ERR_NAME_NOT_RESOLVED + - Cause: the browser is trying to reach the internal Docker hostname directly. + - Fix: use Private mode (NEXT_PUBLIC_USE_PRIVATE_API = "true") so the frontend proxy is used, or set NEXT_PUBLIC_API_BASE_URL to a public URL in Public mode. +- CORS policy errors + - Cause: backend CORS_ORIGINS does not match the actual frontend origin. + - Fix: in Private mode, allow http://bracket-frontend:3000; in Public mode, allow the public frontend domain. +- NEXT_PUBLIC_* changes not reflected + - Cause: these variables are baked into the Next.js build. + - Fix: rebuild the frontend image without cache and restart the container. +- Verifying Private mode in the browser + - In the Network tab, API calls should appear under /api/... on the same origin, not direct calls to the backend hostname. + +# Which mode should I use? +- Use Private mode for most deployments: simpler SSL, no CORS, smaller attack surface. +- Use Public mode when the API must be directly reachable by other clients or services. + +**Default recommendation:** Use **Private Mode** for most deployments. diff --git a/backend/alembic.ini b/backend/alembic.ini index 7ba4f8b9d..a2f4308d3 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -1,7 +1,8 @@ [alembic] # path to migration scripts script_location = alembic -sqlalchemy.url = postgresql://bracket:bracket@localhost/bracket +# URL placeholder - will be overridden by env.py using environm +sqlalchemy.url = driver://user:pass@localhost/dbname # Logging configuration [loggers] diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 02a219e60..f4f8277f8 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -18,7 +18,8 @@ def run_migrations_offline() -> None: - url = ALEMBIC_CONFIG.get_main_option("sqlalchemy.url") + # Use the environment configuration instead of alembic.ini + url = str(config.pg_dsn) context.configure(url=url, target_metadata=Base.metadata, compare_type=True) with context.begin_transaction(): diff --git a/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py b/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py index 5f447ec75..11d3bd813 100644 --- a/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py +++ b/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py @@ -17,7 +17,7 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index("ix_users_email", table_name="users") + op.drop_index("ix_users_email", table_name="users", if_exists=True) op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) op.drop_constraint("users_x_clubs_user_id_fkey", "users_x_clubs", type_="foreignkey") op.drop_constraint("users_x_clubs_club_id_fkey", "users_x_clubs", type_="foreignkey") @@ -36,6 +36,6 @@ def downgrade() -> None: op.create_foreign_key( "users_x_clubs_user_id_fkey", "users_x_clubs", "users", ["user_id"], ["id"] ) - op.drop_index(op.f("ix_users_email"), table_name="users") + op.drop_index(op.f("ix_users_email"), table_name="users", if_exists=True) op.create_index("ix_users_email", "users", ["email"], unique=False) # ### end Alembic commands ### diff --git a/backend/bracket/app.py b/backend/bracket/app.py index 3180a2451..66c0c6d02 100644 --- a/backend/bracket/app.py +++ b/backend/bracket/app.py @@ -40,10 +40,39 @@ @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncIterator[None]: await database.connect() + + # Check database state before any operations + table_count = await database.fetch_val( + "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" + ) + is_completely_empty = table_count == 0 + + # Check if alembic_version table exists (indicates migrations were run before) + alembic_version_exists = await database.fetch_val( + "SELECT EXISTS (SELECT FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_name = 'alembic_version')" + ) + + logger.info(f"Database state: table_count={table_count}, alembic_version_exists={alembic_version_exists}") + + # Initialize database if needed await init_db_when_empty() - + + # Decision logic for migrations: if config.auto_run_migrations and environment is not Environment.CI: - alembic_run_migrations() + if is_completely_empty: + # Fresh database: tables created from schema, no migrations needed + logger.info("Fresh database detected, skipping Alembic migrations (tables already created from schema)") + elif alembic_version_exists: + # Existing database with Alembic history: run migrations to update + logger.info("Existing database with migration history detected, running Alembic migrations...") + alembic_run_migrations() + else: + # Existing database without Alembic history: dangerous, log warning + logger.warning( + "Existing database without Alembic history detected. " + "Manual intervention may be required. Skipping migrations to prevent conflicts." + ) if environment is Environment.PRODUCTION: start_cronjobs() diff --git a/backend/bracket/utils/db_init.py b/backend/bracket/utils/db_init.py index 0c86fdc9f..af1785fd0 100644 --- a/backend/bracket/utils/db_init.py +++ b/backend/bracket/utils/db_init.py @@ -116,20 +116,72 @@ async def create_admin_user() -> UserId: return user.id +async def check_alembic_sync() -> bool: + """Check if the database is in sync with Alembic migrations.""" + try: + # Check if alembic_version table exists + alembic_version_exists = await database.fetch_val( + "SELECT EXISTS (SELECT FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_name = 'alembic_version')" + ) + return bool(alembic_version_exists) + except Exception: + return False + + async def init_db_when_empty() -> UserId | None: table_count = await database.fetch_val( "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" ) + + # Check if we have a completely empty database (no tables at all) + is_completely_empty = table_count == 0 + # Check if Alembic tracking is in place + is_alembic_synced = await check_alembic_sync() + if config.admin_email and config.admin_password: - if (table_count <= 1 and environment != Environment.CI) or ( - environment is Environment.DEVELOPMENT and await get_user(config.admin_email) is None - ): - logger.warning("Empty db detected, creating tables...") - metadata.create_all(engine) - alembic_stamp_head() - - logger.warning("Empty db detected, creating admin user...") - return await create_admin_user() + # Only initialize if database is truly empty OR in development + should_initialize = ( + is_completely_empty or + (environment is Environment.DEVELOPMENT and not is_alembic_synced) + ) + + if should_initialize: + if is_completely_empty: + logger.warning("Completely empty database detected, initializing from schema...") + # Create all tables directly from the schema models + metadata.create_all(engine) + # Mark the database as up-to-date with Alembic + alembic_stamp_head() + logger.warning("Database initialized successfully from schema with Alembic sync") + elif environment is Environment.DEVELOPMENT: + logger.warning("Development environment: ensuring tables exist...") + metadata.create_all(engine) + if not is_alembic_synced: + alembic_stamp_head() + + # Try to create admin user if needed + try: + existing_user = await get_user(config.admin_email) + if existing_user is None: + logger.warning("Creating admin user...") + return await create_admin_user() + else: + logger.info("Admin user already exists") + except Exception as e: + logger.warning(f"Could not check/create admin user: {e}") + + elif table_count > 0: + logger.info(f"Existing database detected with {table_count} tables, skipping initialization") + # In production with existing database, only try to ensure admin user exists + if environment is Environment.PRODUCTION: + try: + existing_user = await get_user(config.admin_email) + if existing_user is None: + logger.warning("Admin user missing in existing database, creating...") + return await create_admin_user() + except Exception as e: + logger.warning(f"Could not check admin user in existing database: {e}") return None diff --git a/docker-compose.yml b/docker-compose.yml index ac7468baf..34199e537 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,14 +11,20 @@ services: depends_on: - postgres environment: - ENVIRONMENT: DEVELOPMENT - CORS_ORIGINS: http://localhost:3000 - PG_DSN: postgresql://bracket_dev:bracket_dev@postgres:5432/bracket_dev + ENVIRONMENT: PRODUCTION + # CORS - Internal communication between frontend and backend + CORS_ORIGINS: http://bracket-frontend:3000 + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: http://192.168.100.10:3000 image: ghcr.io/evroon/bracket-backend networks: - bracket_lan - ports: - - 8400:8400 + # Backend is PRIVATE - Uncomment ports below for PUBLIC API mode + # ports: + # - "8400:8400" restart: unless-stopped volumes: - ./backend/static:/app/static @@ -26,19 +32,27 @@ services: bracket-frontend: container_name: bracket-frontend environment: - NEXT_PUBLIC_API_BASE_URL: http://localhost:8400 - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001 - image: ghcr.io/evroon/bracket-frontend + # Private API mode - Uses internal proxy to communicate with backend + NEXT_PUBLIC_USE_PRIVATE_API: "true" + # Backend URL (for proxy reference) + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 + # Internal URL for proxy (server-side only) + INTERNAL_API_BASE_URL: http://bracket-backend:8400 + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + # Use local image with internal proxy + image: bracket-frontend-local + networks: + - bracket_lan ports: - - 3000:3000 + - "3000:3000" restart: unless-stopped postgres: environment: - POSTGRES_DB: bracket_dev - POSTGRES_PASSWORD: bracket_dev - POSTGRES_USER: bracket_dev - image: postgres + POSTGRES_DB: bracket_prod + POSTGRES_PASSWORD: bracket_prod + POSTGRES_USER: bracket_prod + image: postgres:15 networks: - bracket_lan restart: always diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3dda8be45..caa81fbfc 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -17,7 +17,8 @@ WORKDIR /app COPY . . COPY --from=deps /app/node_modules ./node_modules -RUN NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER \ +RUN NEXT_PUBLIC_USE_PRIVATE_API=true \ + NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER \ NEXT_PUBLIC_HCAPTCHA_SITE_KEY=NEXT_PUBLIC_HCAPTCHA_SITE_KEY_PLACEHOLDER \ yarn build diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 52e831b43..254b73c16 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/src/pages/api/[...path].ts b/frontend/src/pages/api/[...path].ts new file mode 100644 index 000000000..a472ad570 --- /dev/null +++ b/frontend/src/pages/api/[...path].ts @@ -0,0 +1,73 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +const INTERNAL_API_URL = process.env.INTERNAL_API_BASE_URL || 'http://bracket-backend:8400'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { path, ...query } = req.query; + const apiPath = Array.isArray(path) ? path.join('/') : path || ''; + + // Build internal backend URL + const url = `${INTERNAL_API_URL}/${apiPath}`; + + // Build query string only for valid parameters + const validQuery: Record = {}; + Object.entries(query).forEach(([key, value]) => { + if (typeof value === 'string') { + validQuery[key] = value; + } + }); + + const queryString = new URLSearchParams(validQuery).toString(); + const fullUrl = queryString ? `${url}?${queryString}` : url; + + // Configure headers, copying relevant ones from original request + const headers: Record = {}; + + // Copy Content-Type if exists + if (req.headers['content-type']) { + headers['Content-Type'] = req.headers['content-type']; + } + + // Pass Authorization header if exists + if (req.headers.authorization) { + headers.Authorization = req.headers.authorization; + } + + // Pass other relevant headers + if (req.headers.accept) { + headers.Accept = req.headers.accept; + } + + console.log(`[API Proxy] ${req.method} ${fullUrl}`); + + // Make request to internal backend + const response = await axios({ + method: req.method as any, + url: fullUrl, + data: req.method !== 'GET' && req.method !== 'HEAD' ? req.body : undefined, + headers, + timeout: 30000, + validateStatus: () => true, // Don't throw error on 4xx/5xx codes + }); + + // Copy important response headers + if (response.headers['content-type']) { + res.setHeader('Content-Type', response.headers['content-type']); + } + + // Return response + res.status(response.status).json(response.data); + } catch (error: any) { + console.error('[API Proxy] Error:', error.message); + + if (error.code === 'ECONNREFUSED') { + res.status(503).json({ error: 'Backend service unavailable' }); + } else if (error.code === 'ETIMEDOUT') { + res.status(504).json({ error: 'Backend timeout' }); + } else { + res.status(500).json({ error: 'Internal server error' }); + } + } +} \ No newline at end of file diff --git a/frontend/src/services/adapter.tsx b/frontend/src/services/adapter.tsx index 254738380..964250fb5 100644 --- a/frontend/src/services/adapter.tsx +++ b/frontend/src/services/adapter.tsx @@ -53,9 +53,21 @@ export function requestSucceeded(result: AxiosResponse | AxiosError) { } export function getBaseApiUrl() { - return process.env.NEXT_PUBLIC_API_BASE_URL != null - ? process.env.NEXT_PUBLIC_API_BASE_URL - : 'http://localhost:8400'; + // Check if internal proxy should be used (private API) + const usePrivateApi = process.env.NEXT_PUBLIC_USE_PRIVATE_API === 'true'; + + if (usePrivateApi) { + // Private mode: use frontend API routes as proxy + // Users never see /api in URLs, only used internally + return typeof window !== 'undefined' && window.location.origin + ? `${window.location.origin}/api` + : '/api'; + } else { + // Public mode: connect directly to backend + return process.env.NEXT_PUBLIC_API_BASE_URL != null + ? process.env.NEXT_PUBLIC_API_BASE_URL + : 'http://localhost:8400'; + } } export function createAxios() { diff --git a/switch-api-mode.sh b/switch-api-mode.sh new file mode 100755 index 000000000..a9c8425ad --- /dev/null +++ b/switch-api-mode.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Script to switch between public and private API modes +# Usage: ./switch-api-mode.sh [private|public] + +set -e + +MODE=${1:-private} + +case $MODE in + "private") + echo "🔒 Configuring PRIVATE API (secure mode)..." + cp .env.private .env + echo "✅ Backend will NOT be accessible from Internet" + echo "✅ Frontend will use TRANSPARENT internal proxy" + echo "✅ Users will NEVER see /api in URLs" + echo "✅ Only one domain needed: yourdomain.com" + echo "✅ Enhanced security" + ;; + "public") + echo "🌐 Configuring PUBLIC API..." + cp .env.public .env + echo "⚠️ Backend WILL be accessible from Internet" + echo "⚠️ Users will see requests to api.yourdomain.com" + echo "⚠️ Browser connects directly to backend" + echo "⚠️ Requires correct CORS configuration" + echo "⚠️ Two domains required" + ;; + *) + echo "❌ Invalid mode. Use: private or public" + echo "Example: ./switch-api-mode.sh private" + exit 1 + ;; +esac + +echo "" +echo "📝 .env file configured for mode: $MODE" +echo "🐳 Run: docker-compose down && docker-compose up -d" +echo "" + +if [ "$MODE" = "private" ]; then + echo "✅ NGINX CONFIGURATION FOR PRIVATE MODE:" + echo " - Only configure: yourdomain.com → 172.16.0.4:3000" + echo " - DO NOT configure api.yourdomain.com" + echo " - Users only see normal frontend URLs" +elif [ "$MODE" = "public" ]; then + echo "⚠️ IMPORTANT for public mode:" + echo " - Configure nginx for yourdomain.com → 172.16.0.4:3000" + echo " - Configure nginx for api.yourdomain.com → 172.16.0.4:8400" + echo " - Make sure CORS_ORIGINS is correct" +fi \ No newline at end of file