mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-18 21:15:16 +02:00
Merge pull request #1499 from AnishSarkar22/feat/reverse-proxy
feat: Add single-origin reverse proxy deployment with runtime web config
This commit is contained in:
commit
b6d25d3828
84 changed files with 5599 additions and 5315 deletions
7
.github/workflows/desktop-release.yml
vendored
7
.github/workflows/desktop-release.yml
vendored
|
|
@ -95,10 +95,12 @@ jobs:
|
|||
run: pnpm build
|
||||
working-directory: surfsense_web
|
||||
env:
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${{ vars.NEXT_PUBLIC_FASTAPI_BACKEND_URL }}
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${{ vars.HOSTED_BACKEND_URL }}
|
||||
SURFSENSE_BACKEND_INTERNAL_URL: ${{ vars.HOSTED_BACKEND_URL }}
|
||||
NEXT_PUBLIC_ZERO_CACHE_URL: ${{ vars.NEXT_PUBLIC_ZERO_CACHE_URL }}
|
||||
NEXT_PUBLIC_DEPLOYMENT_MODE: ${{ vars.NEXT_PUBLIC_DEPLOYMENT_MODE }}
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${{ vars.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE }}
|
||||
NEXT_PUBLIC_AUTH_TYPE: ${{ vars.NEXT_PUBLIC_AUTH_TYPE }}
|
||||
NEXT_PUBLIC_ETL_SERVICE: ${{ vars.NEXT_PUBLIC_ETL_SERVICE }}
|
||||
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
|
||||
|
||||
- name: Install desktop dependencies
|
||||
|
|
@ -109,6 +111,7 @@ jobs:
|
|||
run: pnpm build
|
||||
working-directory: surfsense_desktop
|
||||
env:
|
||||
HOSTED_BACKEND_URL: ${{ vars.HOSTED_BACKEND_URL }}
|
||||
HOSTED_FRONTEND_URL: ${{ vars.HOSTED_FRONTEND_URL }}
|
||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||
POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
|
||||
|
|
|
|||
5
.github/workflows/docker-build.yml
vendored
5
.github/workflows/docker-build.yml
vendored
|
|
@ -199,11 +199,6 @@ jobs:
|
|||
build-args: |
|
||||
${{ matrix.image == 'backend' && format('USE_CUDA={0}', matrix.use_cuda) || '' }}
|
||||
${{ matrix.image == 'backend' && format('CUDA_EXTRA={0}', matrix.cuda_extra) || '' }}
|
||||
${{ matrix.image == 'web' && 'NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__' || '' }}
|
||||
${{ matrix.image == 'web' && 'NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__' || '' }}
|
||||
${{ matrix.image == 'web' && 'NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__' || '' }}
|
||||
${{ matrix.image == 'web' && 'NEXT_PUBLIC_ZERO_CACHE_URL=__NEXT_PUBLIC_ZERO_CACHE_URL__' || '' }}
|
||||
${{ matrix.image == 'web' && 'NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__' || '' }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
|
|
|
|||
5
.github/workflows/e2e-tests.yml
vendored
5
.github/workflows/e2e-tests.yml
vendored
|
|
@ -27,9 +27,10 @@ jobs:
|
|||
PLAYWRIGHT_TEST_EMAIL: e2e-test@surfsense.net
|
||||
PLAYWRIGHT_TEST_PASSWORD: E2eTestPassword123!
|
||||
# Frontend env: Playwright's webServer (surfsense_web/playwright.config.ts)
|
||||
# spawns `pnpm build && pnpm start` in CI; these get baked into the build.
|
||||
# spawns `pnpm build && pnpm start` in CI.
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL: http://localhost:8000
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: LOCAL
|
||||
SURFSENSE_BACKEND_INTERNAL_URL: http://localhost:8000
|
||||
AUTH_TYPE: LOCAL
|
||||
# Shared secret for the test-only POST /__e2e__/auth/token endpoint.
|
||||
# Must match docker-compose.e2e.yml's backend env (x-backend-env).
|
||||
E2E_MINT_SECRET: e2e-mint-secret-not-for-production
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ SECRET_KEY=replace_me_with_a_random_string
|
|||
# Auth type: LOCAL (email/password) or GOOGLE (OAuth)
|
||||
AUTH_TYPE=LOCAL
|
||||
|
||||
# Deployment mode: self-hosted enables local filesystem connectors; cloud hides them.
|
||||
DEPLOYMENT_MODE=self-hosted
|
||||
|
||||
# Allow new user registrations (TRUE or FALSE)
|
||||
# REGISTRATION_ENABLED=TRUE
|
||||
|
||||
|
|
@ -43,51 +46,47 @@ ETL_SERVICE=DOCLING
|
|||
EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Ports (change to avoid conflicts with other services on your machine)
|
||||
# How You Access SurfSense
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# BACKEND_PORT=8929
|
||||
# FRONTEND_PORT=3929
|
||||
# ZERO_CACHE_PORT=5929
|
||||
# SEARXNG_PORT=8888
|
||||
# FLOWER_PORT=5555
|
||||
|
||||
# ==============================================================================
|
||||
# DEV COMPOSE ONLY (docker-compose.dev.yml)
|
||||
# You only need them only if you are running `docker-compose.dev.yml`.
|
||||
# ==============================================================================
|
||||
|
||||
# -- pgAdmin (database GUI) --
|
||||
# PGADMIN_PORT=5050
|
||||
# PGADMIN_DEFAULT_EMAIL=admin@surfsense.com
|
||||
# PGADMIN_DEFAULT_PASSWORD=surfsense
|
||||
|
||||
# -- Redis exposed port (dev only; Redis is internal-only in prod) --
|
||||
# REDIS_PORT=6379
|
||||
|
||||
# -- WhatsApp bridge exposed port (dev/hybrid only; prod keeps it Docker-internal) --
|
||||
# WHATSAPP_BRIDGE_PORT=9929
|
||||
|
||||
# -- Frontend Build Args --
|
||||
# In dev, the frontend is built from source and these are passed as build args.
|
||||
# In prod, they are automatically derived from AUTH_TYPE, ETL_SERVICE, and the port settings above.
|
||||
# NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
|
||||
# NEXT_PUBLIC_ETL_SERVICE=DOCLING
|
||||
# NEXT_PUBLIC_DEPLOYMENT_MODE=self-hosted
|
||||
# One public URL. Browser traffic stays same-origin and Caddy routes internally.
|
||||
SURFSENSE_PUBLIC_URL=http://localhost:3929
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Custom Domain / Reverse Proxy
|
||||
# Public Ports
|
||||
# ------------------------------------------------------------------------------
|
||||
# ONLY set these if you are serving SurfSense on a real domain via a reverse
|
||||
# proxy (e.g. Caddy, Nginx, Cloudflare Tunnel).
|
||||
# For standard localhost deployments, leave all of these commented out.
|
||||
# they are automatically derived from the port settings above.
|
||||
# Production Docker exposes only Caddy to your machine. Caddy then routes
|
||||
# frontend, backend, and zero-cache traffic internally.
|
||||
#
|
||||
# NEXT_FRONTEND_URL=https://app.yourdomain.com
|
||||
# BACKEND_URL=https://api.yourdomain.com
|
||||
# NEXT_PUBLIC_FASTAPI_BACKEND_URL=https://api.yourdomain.com
|
||||
# NEXT_PUBLIC_ZERO_CACHE_URL=https://zero.yourdomain.com
|
||||
# FASTAPI_BACKEND_INTERNAL_URL=http://backend:8000
|
||||
# Local default: LISTEN_HTTP_PORT=3929
|
||||
# Domain default: LISTEN_HTTP_PORT=80 and LISTEN_HTTPS_PORT=443
|
||||
LISTEN_HTTP_PORT=3929
|
||||
LISTEN_HTTPS_PORT=443
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Custom Domain / HTTPS
|
||||
# ------------------------------------------------------------------------------
|
||||
# Leave SURFSENSE_SITE_ADDRESS as :80 for local HTTP.
|
||||
# Set it to your domain to enable automatic HTTPS:
|
||||
# SURFSENSE_SITE_ADDRESS=surf.example.com
|
||||
# CERT_EMAIL=you@example.com
|
||||
SURFSENSE_SITE_ADDRESS=:80
|
||||
CERT_EMAIL=
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Advanced Reverse Proxy Settings
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usually do not change these. They are for custom certificate setup, CDNs/load
|
||||
# balancers, trusted proxy IPs, or changing upload limits.
|
||||
#
|
||||
# CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
|
||||
# CERT_ACME_DNS=
|
||||
# If a CDN/load balancer sits in front of Caddy, narrow this to that proxy's CIDRs.
|
||||
# TRUSTED_PROXIES=0.0.0.0/0
|
||||
# SURFSENSE_MAX_BODY_SIZE=5GB
|
||||
#
|
||||
# Browser API and Zero URLs are same-origin relative behind bundled Caddy.
|
||||
# Next.js server-side calls use Docker DNS through SURFSENSE_BACKEND_INTERNAL_URL
|
||||
# set internally by docker-compose.yml. Usually do not override it.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Zero-cache (real-time sync)
|
||||
|
|
@ -108,10 +107,9 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
|||
|
||||
# Sync worker tuning. zero-cache defaults ZERO_NUM_SYNC_WORKERS to the number
|
||||
# of CPU cores, which can exceed the connection pool limits on high-core machines.
|
||||
# Each sync worker needs at least 1 connection from both the UPSTREAM and CVR
|
||||
# pools, so these constraints must hold:
|
||||
# ZERO_UPSTREAM_MAX_CONNS >= ZERO_NUM_SYNC_WORKERS
|
||||
# ZERO_CVR_MAX_CONNS >= ZERO_NUM_SYNC_WORKERS
|
||||
# Each sync worker needs at least 1 connection from both the UPSTREAM and CVR pools.
|
||||
# Keep ZERO_UPSTREAM_MAX_CONNS and ZERO_CVR_MAX_CONNS greater than or equal to
|
||||
# ZERO_NUM_SYNC_WORKERS.
|
||||
# Default of 4 workers is sufficient for self-hosted / personal use.
|
||||
# ZERO_NUM_SYNC_WORKERS=4
|
||||
# ZERO_UPSTREAM_MAX_CONNS=20
|
||||
|
|
@ -125,16 +123,16 @@ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
|||
|
||||
# ZERO_QUERY_URL: where zero-cache forwards query requests for resolution.
|
||||
# ZERO_MUTATE_URL: required by zero-cache when auth tokens are used, even though
|
||||
# SurfSense does not use Zero mutators. Setting both URLs tells zero-cache to
|
||||
# skip its own JWT verification and let the app endpoints handle auth instead.
|
||||
# The mutate endpoint is a no-op that returns an empty response.
|
||||
# SurfSense does not use Zero mutators. Setting both URLs tells zero-cache to
|
||||
# skip its own JWT verification and let the app endpoints handle auth instead.
|
||||
# The mutate endpoint is a no-op that returns an empty response.
|
||||
# Default: Docker service networking (http://frontend:3000/api/zero/...).
|
||||
# Override when running the frontend outside Docker:
|
||||
# ZERO_QUERY_URL=http://host.docker.internal:3000/api/zero/query
|
||||
# ZERO_MUTATE_URL=http://host.docker.internal:3000/api/zero/mutate
|
||||
# Override for custom domain:
|
||||
# ZERO_QUERY_URL=https://app.yourdomain.com/api/zero/query
|
||||
# ZERO_MUTATE_URL=https://app.yourdomain.com/api/zero/mutate
|
||||
# ZERO_QUERY_URL=http://host.docker.internal:3000/api/zero/query
|
||||
# ZERO_MUTATE_URL=http://host.docker.internal:3000/api/zero/mutate
|
||||
# Override for custom domain only when zero-cache is not in the bundled Docker network:
|
||||
# ZERO_QUERY_URL=https://surf.example.com/api/zero/query
|
||||
# ZERO_MUTATE_URL=https://surf.example.com/api/zero/mutate
|
||||
# ZERO_QUERY_URL=http://frontend:3000/api/zero/query
|
||||
# ZERO_MUTATE_URL=http://frontend:3000/api/zero/mutate
|
||||
|
||||
|
|
@ -222,73 +220,74 @@ STT_SERVICE=local/base
|
|||
# ------------------------------------------------------------------------------
|
||||
|
||||
# -- Google Connectors --
|
||||
# GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/connector/callback
|
||||
# GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback
|
||||
# GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback
|
||||
# GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:3929/api/v1/auth/google/calendar/connector/callback
|
||||
# GOOGLE_GMAIL_REDIRECT_URI=http://localhost:3929/api/v1/auth/google/gmail/connector/callback
|
||||
# GOOGLE_DRIVE_REDIRECT_URI=http://localhost:3929/api/v1/auth/google/drive/connector/callback
|
||||
|
||||
# -- Notion --
|
||||
# NOTION_CLIENT_ID=
|
||||
# NOTION_CLIENT_SECRET=
|
||||
# NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
|
||||
# NOTION_REDIRECT_URI=http://localhost:3929/api/v1/auth/notion/connector/callback
|
||||
|
||||
# -- Slack --
|
||||
# SLACK_CLIENT_ID=
|
||||
# SLACK_CLIENT_SECRET=
|
||||
# SLACK_REDIRECT_URI=http://localhost:8000/api/v1/auth/slack/connector/callback
|
||||
# SLACK_REDIRECT_URI=http://localhost:3929/api/v1/auth/slack/connector/callback
|
||||
|
||||
# -- Discord --
|
||||
# DISCORD_CLIENT_ID=
|
||||
# DISCORD_CLIENT_SECRET=
|
||||
# DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback
|
||||
# DISCORD_REDIRECT_URI=http://localhost:3929/api/v1/auth/discord/connector/callback
|
||||
# DISCORD_BOT_TOKEN=
|
||||
|
||||
# -- Atlassian (Jira & Confluence) --
|
||||
# ATLASSIAN_CLIENT_ID=
|
||||
# ATLASSIAN_CLIENT_SECRET=
|
||||
# JIRA_REDIRECT_URI=http://localhost:8000/api/v1/auth/jira/connector/callback
|
||||
# CONFLUENCE_REDIRECT_URI=http://localhost:8000/api/v1/auth/confluence/connector/callback
|
||||
# JIRA_REDIRECT_URI=http://localhost:3929/api/v1/auth/jira/connector/callback
|
||||
# CONFLUENCE_REDIRECT_URI=http://localhost:3929/api/v1/auth/confluence/connector/callback
|
||||
|
||||
# -- Linear --
|
||||
# LINEAR_CLIENT_ID=
|
||||
# LINEAR_CLIENT_SECRET=
|
||||
# LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback
|
||||
# LINEAR_REDIRECT_URI=http://localhost:3929/api/v1/auth/linear/connector/callback
|
||||
|
||||
# -- ClickUp --
|
||||
# CLICKUP_CLIENT_ID=
|
||||
# CLICKUP_CLIENT_SECRET=
|
||||
# CLICKUP_REDIRECT_URI=http://localhost:8000/api/v1/auth/clickup/connector/callback
|
||||
# CLICKUP_REDIRECT_URI=http://localhost:3929/api/v1/auth/clickup/connector/callback
|
||||
|
||||
# -- Airtable --
|
||||
# AIRTABLE_CLIENT_ID=
|
||||
# AIRTABLE_CLIENT_SECRET=
|
||||
# AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback
|
||||
# AIRTABLE_REDIRECT_URI=http://localhost:3929/api/v1/auth/airtable/connector/callback
|
||||
|
||||
# -- Microsoft OAuth (Teams & OneDrive) --
|
||||
# MICROSOFT_CLIENT_ID=
|
||||
# MICROSOFT_CLIENT_SECRET=
|
||||
# TEAMS_REDIRECT_URI=http://localhost:8000/api/v1/auth/teams/connector/callback
|
||||
# ONEDRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/onedrive/connector/callback
|
||||
# TEAMS_REDIRECT_URI=http://localhost:3929/api/v1/auth/teams/connector/callback
|
||||
# ONEDRIVE_REDIRECT_URI=http://localhost:3929/api/v1/auth/onedrive/connector/callback
|
||||
|
||||
# -- Dropbox --
|
||||
# DROPBOX_APP_KEY=
|
||||
# DROPBOX_APP_SECRET=
|
||||
# DROPBOX_REDIRECT_URI=http://localhost:8000/api/v1/auth/dropbox/connector/callback
|
||||
# DROPBOX_REDIRECT_URI=http://localhost:3929/api/v1/auth/dropbox/connector/callback
|
||||
|
||||
# -- Composio --
|
||||
# COMPOSIO_API_KEY=
|
||||
# COMPOSIO_ENABLED=TRUE
|
||||
# COMPOSIO_REDIRECT_URI=http://localhost:8000/api/v1/auth/composio/connector/callback
|
||||
# COMPOSIO_REDIRECT_URI=http://localhost:3929/api/v1/auth/composio/connector/callback
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Messaging Channels (optional)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Configure only the external chat channels you want to use.
|
||||
# GATEWAY_ENABLED=TRUE
|
||||
|
||||
# -- Telegram --
|
||||
# TELEGRAM_SHARED_BOT_TOKEN=
|
||||
# TELEGRAM_SHARED_BOT_USERNAME=
|
||||
# TELEGRAM_WEBHOOK_SECRET=
|
||||
# GATEWAY_BASE_URL=http://localhost:8929
|
||||
# GATEWAY_BASE_URL=http://localhost:3929
|
||||
# GATEWAY_TELEGRAM_INTAKE_MODE=webhook
|
||||
|
||||
# -- WhatsApp --
|
||||
|
|
@ -307,20 +306,20 @@ STT_SERVICE=local/base
|
|||
#
|
||||
# GATEWAY_SLACK_ENABLED=FALSE
|
||||
# GATEWAY_SLACK_SIGNING_SECRET=
|
||||
# GATEWAY_SLACK_REDIRECT_URI=http://localhost:8929/api/v1/gateway/slack/callback
|
||||
# GATEWAY_SLACK_REDIRECT_URI=http://localhost:3929/api/v1/gateway/slack/callback
|
||||
|
||||
# -- Discord --
|
||||
# Uses DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, and DISCORD_BOT_TOKEN from the
|
||||
# Discord connector section.
|
||||
#
|
||||
# GATEWAY_DISCORD_ENABLED=FALSE
|
||||
# GATEWAY_DISCORD_REDIRECT_URI=http://localhost:8929/api/v1/gateway/discord/callback
|
||||
# GATEWAY_DISCORD_REDIRECT_URI=http://localhost:3929/api/v1/gateway/discord/callback
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# SearXNG (bundled web search, works out of the box with no config needed)
|
||||
# ------------------------------------------------------------------------------
|
||||
# SearXNG provides web search to all search spaces automatically.
|
||||
# To access the SearXNG UI directly: http://localhost:8888
|
||||
# To access the SearXNG UI directly in dev/deps-only compose: http://localhost:8888
|
||||
# To disable the service entirely: docker compose up --scale searxng=0
|
||||
# To point at your own SearXNG instance instead of the bundled one:
|
||||
# SEARXNG_DEFAULT_HOST=http://your-searxng:8080
|
||||
|
|
@ -457,3 +456,36 @@ NOLOGIN_MODE_ENABLED=FALSE
|
|||
# RESIDENTIAL_PROXY_HOSTNAME=
|
||||
# RESIDENTIAL_PROXY_LOCATION=
|
||||
# RESIDENTIAL_PROXY_TYPE=1
|
||||
|
||||
# ==============================================================================
|
||||
# DEV / DEPS-ONLY COMPOSE OVERRIDES
|
||||
# These are only needed for docker-compose.dev.yml or docker-compose.deps-only.yml.
|
||||
# Production Docker exposes Caddy only; raw app ports below do not affect
|
||||
# docker-compose.yml.
|
||||
# ==============================================================================
|
||||
|
||||
# -- pgAdmin (database GUI, dev/deps-only only) --
|
||||
# PGADMIN_PORT=5050
|
||||
# PGADMIN_DEFAULT_EMAIL=admin@surfsense.com
|
||||
# PGADMIN_DEFAULT_PASSWORD=surfsense
|
||||
|
||||
# -- Redis exposed port (dev/deps-only only; Redis is internal-only in prod) --
|
||||
# REDIS_PORT=6379
|
||||
|
||||
# -- SearXNG exposed port (dev/deps-only only; internal-only in prod) --
|
||||
# SEARXNG_PORT=8888
|
||||
|
||||
# -- WhatsApp bridge exposed port (dev/hybrid only; prod keeps it Docker-internal) --
|
||||
# WHATSAPP_BRIDGE_PORT=9929
|
||||
|
||||
# -- Raw app ports (dev/deps-only only; prod exposes Caddy instead) --
|
||||
# BACKEND_PORT=8000
|
||||
# FRONTEND_PORT=3000
|
||||
# ZERO_CACHE_PORT=4848
|
||||
|
||||
# -- Frontend runtime flags (prod and dev compose) --
|
||||
# The frontend reads these at request time in Docker; no NEXT_PUBLIC_* rebuild
|
||||
# or startup substitution is required.
|
||||
# AUTH_TYPE=LOCAL
|
||||
# ETL_SERVICE=DOCLING
|
||||
# DEPLOYMENT_MODE=self-hosted
|
||||
|
|
|
|||
|
|
@ -253,16 +253,15 @@ services:
|
|||
frontend:
|
||||
build:
|
||||
context: ../surfsense_web
|
||||
args:
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}
|
||||
NEXT_PUBLIC_ETL_SERVICE: ${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}
|
||||
NEXT_PUBLIC_ZERO_CACHE_URL: ${NEXT_PUBLIC_ZERO_CACHE_URL:-http://localhost:${ZERO_CACHE_PORT:-4848}}
|
||||
NEXT_PUBLIC_DEPLOYMENT_MODE: ${NEXT_PUBLIC_DEPLOYMENT_MODE:-self-hosted}
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3000}:3000"
|
||||
env_file:
|
||||
- ../surfsense_web/.env
|
||||
environment:
|
||||
AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
|
||||
ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
|
||||
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
|
||||
SURFSENSE_BACKEND_INTERNAL_URL: http://backend:8000
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
54
docker/docker-compose.proxy.yml
Normal file
54
docker/docker-compose.proxy.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# =============================================================================
|
||||
# SurfSense — Optional Caddy reverse-proxy overlay
|
||||
# =============================================================================
|
||||
# Usage (from docker/):
|
||||
# PROXY_HTTP_PORT=8080 SURFSENSE_PUBLIC_URL=http://localhost:8080 \
|
||||
# docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
|
||||
#
|
||||
# This overlay is for validation and custom deployments. The production
|
||||
# docker-compose.yml includes Caddy by default.
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8929}:8000"
|
||||
|
||||
zero-cache:
|
||||
ports:
|
||||
- "${ZERO_CACHE_PORT:-5929}:4848"
|
||||
|
||||
frontend:
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3929}:3000"
|
||||
|
||||
proxy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PROXY_HTTP_PORT:-8080}:80"
|
||||
- "${PROXY_HTTPS_PORT:-8443}:443"
|
||||
volumes:
|
||||
- ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
environment:
|
||||
SURFSENSE_SITE_ADDRESS: ${SURFSENSE_SITE_ADDRESS:-:80}
|
||||
CERT_EMAIL: ${CERT_EMAIL:-}
|
||||
CERT_ACME_CA: ${CERT_ACME_CA:-https://acme-v02.api.letsencrypt.org/directory}
|
||||
CERT_ACME_DNS: ${CERT_ACME_DNS:-}
|
||||
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-0.0.0.0/0}
|
||||
SURFSENSE_MAX_BODY_SIZE: ${SURFSENSE_MAX_BODY_SIZE:-5GB}
|
||||
depends_on:
|
||||
frontend:
|
||||
condition: service_started
|
||||
backend:
|
||||
condition: service_healthy
|
||||
zero-cache:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
name: surfsense-caddy-data
|
||||
caddy_config:
|
||||
name: surfsense-caddy-config
|
||||
|
|
@ -94,10 +94,39 @@ services:
|
|||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Single public entry point for the Docker stack. Comment this service out
|
||||
# only if you front SurfSense with your own reverse proxy.
|
||||
proxy:
|
||||
image: caddy:2-alpine
|
||||
# For DNS-01/wildcard certificates, replace image with:
|
||||
# build: ./proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${LISTEN_HTTP_PORT:-3929}:80"
|
||||
- "${LISTEN_HTTPS_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
environment:
|
||||
SURFSENSE_SITE_ADDRESS: ${SURFSENSE_SITE_ADDRESS:-:80}
|
||||
CERT_EMAIL: ${CERT_EMAIL:-}
|
||||
CERT_ACME_CA: ${CERT_ACME_CA:-https://acme-v02.api.letsencrypt.org/directory}
|
||||
CERT_ACME_DNS: ${CERT_ACME_DNS:-}
|
||||
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-0.0.0.0/0}
|
||||
SURFSENSE_MAX_BODY_SIZE: ${SURFSENSE_MAX_BODY_SIZE:-5GB}
|
||||
depends_on:
|
||||
frontend:
|
||||
condition: service_started
|
||||
backend:
|
||||
condition: service_healthy
|
||||
zero-cache:
|
||||
condition: service_healthy
|
||||
|
||||
backend:
|
||||
image: ghcr.io/modsetter/surfsense-backend:${SURFSENSE_VERSION:-latest}${SURFSENSE_VARIANT:+-${SURFSENSE_VARIANT}}
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8929}:8000"
|
||||
expose:
|
||||
- "8000"
|
||||
volumes:
|
||||
- shared_temp:/shared_tmp
|
||||
env_file:
|
||||
|
|
@ -113,7 +142,8 @@ services:
|
|||
PYTHONPATH: /app
|
||||
UVICORN_LOOP: asyncio
|
||||
UNSTRUCTURED_HAS_PATCHED_LOOP: "1"
|
||||
NEXT_FRONTEND_URL: ${NEXT_FRONTEND_URL:-http://localhost:${FRONTEND_PORT:-3929}}
|
||||
NEXT_FRONTEND_URL: ${NEXT_FRONTEND_URL:-${SURFSENSE_PUBLIC_URL:-http://localhost:${LISTEN_HTTP_PORT:-3929}}}
|
||||
BACKEND_URL: ${BACKEND_URL:-${SURFSENSE_PUBLIC_URL:-http://localhost:${LISTEN_HTTP_PORT:-3929}}}
|
||||
SEARXNG_DEFAULT_HOST: ${SEARXNG_DEFAULT_HOST:-http://searxng:8080}
|
||||
WHATSAPP_BRIDGE_URL: ${WHATSAPP_BRIDGE_URL:-http://whatsapp-bridge:9929}
|
||||
# Daytona Sandbox – uncomment and set credentials to enable cloud code execution
|
||||
|
|
@ -217,8 +247,8 @@ services:
|
|||
|
||||
zero-cache:
|
||||
image: rocicorp/zero:1.4.0
|
||||
ports:
|
||||
- "${ZERO_CACHE_PORT:-5929}:4848"
|
||||
expose:
|
||||
- "4848"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
|
|
@ -252,16 +282,13 @@ services:
|
|||
|
||||
frontend:
|
||||
image: ghcr.io/modsetter/surfsense-web:${SURFSENSE_VERSION:-latest}
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3929}:3000"
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8929}}
|
||||
NEXT_PUBLIC_ZERO_CACHE_URL: ${NEXT_PUBLIC_ZERO_CACHE_URL:-http://localhost:${ZERO_CACHE_PORT:-5929}}
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
|
||||
NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
|
||||
NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
|
||||
NEXT_PUBLIC_WHATSAPP_DISPLAY_PHONE_NUMBER: ${WHATSAPP_SHARED_DISPLAY_PHONE_NUMBER:-}
|
||||
FASTAPI_BACKEND_INTERNAL_URL: ${FASTAPI_BACKEND_INTERNAL_URL:-http://backend:8000}
|
||||
AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
|
||||
ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
|
||||
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
|
||||
SURFSENSE_BACKEND_INTERNAL_URL: http://backend:8000
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
depends_on:
|
||||
|
|
@ -280,5 +307,9 @@ volumes:
|
|||
name: surfsense-shared-temp
|
||||
zero_cache_data:
|
||||
name: surfsense-zero-cache
|
||||
caddy_data:
|
||||
name: surfsense-caddy-data
|
||||
caddy_config:
|
||||
name: surfsense-caddy-config
|
||||
whatsapp_sessions:
|
||||
name: surfsense-whatsapp-sessions
|
||||
|
|
|
|||
45
docker/proxy/Caddyfile
Normal file
45
docker/proxy/Caddyfile
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
# Optional ACME/global settings. These are harmless in the default :80
|
||||
# localhost mode and become active when SURFSENSE_SITE_ADDRESS is a domain.
|
||||
{$CERT_EMAIL}
|
||||
acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory}
|
||||
{$CERT_ACME_DNS}
|
||||
servers {
|
||||
client_ip_headers X-Forwarded-For X-Real-IP
|
||||
trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0}
|
||||
}
|
||||
}
|
||||
|
||||
(surfsense_proxy) {
|
||||
request_body {
|
||||
max_size {$SURFSENSE_MAX_BODY_SIZE:5GB}
|
||||
}
|
||||
|
||||
# Frontend-owned auth page (the post-login token handler). More specific than
|
||||
# /auth/*, so Caddy's matcher-specificity sort routes it here, not to backend.
|
||||
reverse_proxy /auth/callback* frontend:3000
|
||||
|
||||
# Backend auth routes (FastAPI Users + OAuth helpers).
|
||||
reverse_proxy /auth/* backend:8000
|
||||
|
||||
# Backend user profile routes (FastAPI Users users router, mounted at /users).
|
||||
reverse_proxy /users/* backend:8000
|
||||
|
||||
# Backend REST, streaming, connector OAuth, and messaging gateway endpoints.
|
||||
# FastAPI already serves /api/v1, so the path is forwarded unchanged.
|
||||
reverse_proxy /api/v1/* backend:8000 {
|
||||
flush_interval -1
|
||||
}
|
||||
|
||||
# Zero accepts a single path-component base URL (Zero >= 0.6).
|
||||
# Preserve /zero so browser cacheURL can be ${SURFSENSE_PUBLIC_URL}/zero.
|
||||
reverse_proxy /zero/* zero-cache:4848
|
||||
|
||||
# Next.js app and frontend-owned API routes:
|
||||
# /api/zero/*, /api/search, /api/contact, etc.
|
||||
reverse_proxy /* frontend:3000
|
||||
}
|
||||
|
||||
{$SURFSENSE_SITE_ADDRESS::80} {
|
||||
import surfsense_proxy
|
||||
}
|
||||
10
docker/proxy/Dockerfile
Normal file
10
docker/proxy/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
FROM caddy:2-builder-alpine AS builder
|
||||
|
||||
RUN xcaddy build \
|
||||
--with github.com/caddy-dns/cloudflare \
|
||||
--with github.com/caddy-dns/digitalocean
|
||||
|
||||
FROM caddy:2-alpine
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
|
@ -333,11 +333,13 @@ step "Downloading SurfSense files"
|
|||
info "Installation directory: ${INSTALL_DIR}"
|
||||
mkdir -p "${INSTALL_DIR}/scripts"
|
||||
mkdir -p "${INSTALL_DIR}/searxng"
|
||||
mkdir -p "${INSTALL_DIR}/proxy"
|
||||
|
||||
FILES=(
|
||||
"docker/docker-compose.yml:docker-compose.yml"
|
||||
"docker/docker-compose.gpu.yml:docker-compose.gpu.yml"
|
||||
"docker/.env.example:.env.example"
|
||||
"docker/proxy/Caddyfile:proxy/Caddyfile"
|
||||
"docker/postgresql.conf:postgresql.conf"
|
||||
"docker/scripts/migrate-database.sh:scripts/migrate-database.sh"
|
||||
"docker/searxng/settings.yml:searxng/settings.yml"
|
||||
|
|
@ -532,9 +534,12 @@ _variant_display=$(grep '^SURFSENSE_VARIANT=' "${INSTALL_DIR}/.env" 2>/dev/null
|
|||
_variant_display="${_variant_display:-cpu}"
|
||||
step "SurfSense is now installed [${_version_display}]"
|
||||
|
||||
info " Frontend: http://localhost:3929"
|
||||
info " Backend: http://localhost:8929"
|
||||
info " API Docs: http://localhost:8929/docs"
|
||||
_public_url=$(grep '^SURFSENSE_PUBLIC_URL=' "${INSTALL_DIR}/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | head -1 || true)
|
||||
_public_url="${_public_url:-http://localhost:3929}"
|
||||
|
||||
info " SurfSense: ${_public_url}"
|
||||
info " Backend: ${_public_url}/api/v1"
|
||||
info " Zero sync: ${_public_url}/zero"
|
||||
info ""
|
||||
info " Config: ${INSTALL_DIR}/.env"
|
||||
info " Variant: ${_variant_display}"
|
||||
|
|
|
|||
|
|
@ -15,12 +15,9 @@ CELERY_TASK_DEFAULT_QUEUE=surfsense
|
|||
# Optional: TTL in seconds for connector indexing lock key
|
||||
# CONNECTOR_INDEXING_LOCK_TTL_SECONDS=28800
|
||||
|
||||
# Messaging Gateway (global)
|
||||
# GATEWAY_ENABLED: master switch for ALL messaging gateway channels (Telegram, WhatsApp,
|
||||
# Slack, Discord). When FALSE, no gateway background workers/supervisors start and all
|
||||
# gateway HTTP routes (webhooks, OAuth callbacks, pairing) return 404. Set per-channel
|
||||
# flags below to control individual platforms once the gateway is enabled.
|
||||
GATEWAY_ENABLED=TRUE
|
||||
# Messaging Gateway: disabled by default; set TRUE to enable chat integrations.
|
||||
# Supported messaging gateways: WhatsApp, Telegram, Discord, Slack
|
||||
# GATEWAY_ENABLED=TRUE
|
||||
|
||||
# Telegram Gateway
|
||||
# TELEGRAM_WEBHOOK_SECRET must be 1-256 chars and contain only A-Z, a-z, 0-9, _ or -
|
||||
|
|
@ -386,7 +383,9 @@ LANGSMITH_PROJECT=surfsense
|
|||
# SURFSENSE_ENABLE_LLM_TOOL_SELECTOR=false # adds a per-turn LLM call
|
||||
|
||||
# Observability - OTel
|
||||
# SURFSENSE_ENABLE_OTEL=false
|
||||
# Disabled by default. Uncomment to enable OpenTelemetry.
|
||||
# SURFSENSE_ENABLE_OTEL=true
|
||||
|
||||
# OpenTelemetry - endpoint enables export; absent = no-op.
|
||||
# Production should point at an OTel Collector. For local docker-compose.dev.yml,
|
||||
# use http://otel-lgtm:4317 instead.
|
||||
|
|
|
|||
|
|
@ -535,14 +535,15 @@ class Config:
|
|||
# Platform web search (SearXNG)
|
||||
SEARXNG_DEFAULT_HOST = os.getenv("SEARXNG_DEFAULT_HOST")
|
||||
|
||||
NEXT_FRONTEND_URL = os.getenv("NEXT_FRONTEND_URL")
|
||||
SURFSENSE_PUBLIC_URL = os.getenv("SURFSENSE_PUBLIC_URL")
|
||||
NEXT_FRONTEND_URL = os.getenv("NEXT_FRONTEND_URL") or SURFSENSE_PUBLIC_URL
|
||||
# Backend URL to override the http to https in the OAuth redirect URI
|
||||
BACKEND_URL = os.getenv("BACKEND_URL")
|
||||
BACKEND_URL = os.getenv("BACKEND_URL") or SURFSENSE_PUBLIC_URL
|
||||
|
||||
# Messaging gateway (Telegram v1)
|
||||
# Messaging gateway
|
||||
# Global master switch: when FALSE, no gateway supervisors/workers start and all
|
||||
# gateway HTTP routes return 404, regardless of the per-channel flags below.
|
||||
GATEWAY_ENABLED = os.getenv("GATEWAY_ENABLED", "TRUE").upper() == "TRUE"
|
||||
# gated gateway HTTP routes return 404, regardless of the per-channel flags below.
|
||||
GATEWAY_ENABLED = os.getenv("GATEWAY_ENABLED", "FALSE").upper() == "TRUE"
|
||||
TELEGRAM_SHARED_BOT_TOKEN = os.getenv("TELEGRAM_SHARED_BOT_TOKEN")
|
||||
TELEGRAM_SHARED_BOT_USERNAME = os.getenv("TELEGRAM_SHARED_BOT_USERNAME")
|
||||
TELEGRAM_WEBHOOK_SECRET = os.getenv("TELEGRAM_WEBHOOK_SECRET")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from app.config import config
|
|||
|
||||
|
||||
def require_gateway_enabled() -> None:
|
||||
"""FastAPI dependency that gates all gateway HTTP routes on the global flag.
|
||||
"""FastAPI dependency that gates gateway operational routes on the global flag.
|
||||
|
||||
Returns 404 (rather than 503) when ``GATEWAY_ENABLED`` is FALSE so that
|
||||
disabling the gateway makes its webhook/OAuth/pairing surface indistinguishable
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ from .dropbox_add_connector_route import router as dropbox_add_connector_router
|
|||
from .editor_routes import router as editor_router
|
||||
from .export_routes import router as export_router
|
||||
from .folders_routes import router as folders_router
|
||||
from .gateway_webhook_routes import router as gateway_router
|
||||
from .gateway_webhook_routes import (
|
||||
config_router as gateway_config_router,
|
||||
router as gateway_router,
|
||||
)
|
||||
from .gateway_whatsapp_baileys_routes import router as gateway_whatsapp_baileys_router
|
||||
from .gateway_whatsapp_webhook_routes import router as gateway_whatsapp_webhook_router
|
||||
from .google_calendar_add_connector_route import (
|
||||
|
|
@ -74,6 +77,7 @@ router.include_router(export_router)
|
|||
router.include_router(documents_router)
|
||||
router.include_router(folders_router)
|
||||
_gateway_enabled_dep = [Depends(require_gateway_enabled)]
|
||||
router.include_router(gateway_config_router)
|
||||
router.include_router(gateway_router, dependencies=_gateway_enabled_dep)
|
||||
router.include_router(
|
||||
gateway_whatsapp_webhook_router, dependencies=_gateway_enabled_dep
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ from app.utils.oauth_security import OAuthStateManager, TokenEncryption
|
|||
from app.utils.rbac import check_search_space_access
|
||||
|
||||
router = APIRouter(prefix="/gateway", tags=["gateway"])
|
||||
config_router = APIRouter(prefix="/gateway", tags=["gateway"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SLACK_AUTHORIZATION_URL = "https://slack.com/oauth/v2/authorize"
|
||||
|
|
@ -967,11 +968,20 @@ async def list_platforms(
|
|||
]
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
@config_router.get("/config")
|
||||
async def get_gateway_config(
|
||||
user: User = Depends(current_active_user),
|
||||
) -> dict[str, bool | str]:
|
||||
if not config.GATEWAY_ENABLED:
|
||||
return {
|
||||
"enabled": False,
|
||||
"telegram_enabled": False,
|
||||
"whatsapp_intake_mode": "disabled",
|
||||
"slack_enabled": False,
|
||||
"discord_enabled": False,
|
||||
}
|
||||
return {
|
||||
"enabled": True,
|
||||
"telegram_enabled": _telegram_gateway_enabled(),
|
||||
"whatsapp_intake_mode": config.GATEWAY_WHATSAPP_INTAKE_MODE,
|
||||
"slack_enabled": _slack_gateway_enabled(),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ dependencies = [
|
|||
"elasticsearch>=9.1.1",
|
||||
"faster-whisper>=1.1.0",
|
||||
"celery[redis]>=5.5.3",
|
||||
"flower>=2.0.1",
|
||||
"redis>=5.2.1",
|
||||
"firecrawl-py>=4.9.0",
|
||||
"boto3>=1.35.0",
|
||||
|
|
|
|||
9163
surfsense_backend/uv.lock
generated
9163
surfsense_backend/uv.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -108,8 +108,11 @@ async function buildElectron() {
|
|||
sourcemap: true,
|
||||
minify: false,
|
||||
define: {
|
||||
'process.env.HOSTED_BACKEND_URL': JSON.stringify(
|
||||
process.env.HOSTED_BACKEND_URL || desktopEnv.HOSTED_BACKEND_URL || ''
|
||||
),
|
||||
'process.env.HOSTED_FRONTEND_URL': JSON.stringify(
|
||||
process.env.HOSTED_FRONTEND_URL || desktopEnv.HOSTED_FRONTEND_URL || 'https://surfsense.net'
|
||||
process.env.HOSTED_FRONTEND_URL || desktopEnv.HOSTED_FRONTEND_URL || 'https://surfsense.com'
|
||||
),
|
||||
'process.env.POSTHOG_KEY': JSON.stringify(
|
||||
process.env.POSTHOG_KEY || desktopEnv.POSTHOG_KEY || ''
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ export async function startNextServer(): Promise<void> {
|
|||
|
||||
const standalonePath = getStandalonePath();
|
||||
const serverScript = path.join(standalonePath, 'server.js');
|
||||
const backendInternalUrl = process.env.SURFSENSE_BACKEND_INTERNAL_URL || process.env.HOSTED_BACKEND_URL;
|
||||
|
||||
const child = utilityProcess.fork(serverScript, [], {
|
||||
cwd: standalonePath,
|
||||
env: {
|
||||
...process.env,
|
||||
...(backendInternalUrl ? { SURFSENSE_BACKEND_INTERNAL_URL: backendInternalUrl } : {}),
|
||||
PORT: String(serverPort),
|
||||
// Loopback bind: avoids 0.0.0.0 leaking into request.url and redirect origins.
|
||||
HOSTNAME: SERVER_HOST,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
|
||||
# Optional packaged-client override. Leave unset in Docker so browser requests
|
||||
# use same-origin relative URLs behind Caddy.
|
||||
# NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
|
||||
|
||||
# Server-only. Internal backend URL used by Next.js server code.
|
||||
FASTAPI_BACKEND_INTERNAL_URL=https://your-internal-backend.example.com
|
||||
SURFSENSE_BACKEND_INTERNAL_URL=http://backend:8000
|
||||
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE
|
||||
NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
|
||||
NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
||||
AUTH_TYPE=LOCAL
|
||||
ETL_SERVICE=DOCLING
|
||||
DEPLOYMENT_MODE=self-hosted
|
||||
|
||||
# Contact Form Vars (optional)
|
||||
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres
|
||||
|
||||
# Deployment mode (optional)
|
||||
NEXT_PUBLIC_DEPLOYMENT_MODE="self-hosted" or "cloud"
|
||||
|
||||
# PostHog analytics (optional, leave empty to disable)
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
|
|
|
|||
|
|
@ -35,21 +35,6 @@ RUN apk add --no-cache git
|
|||
# Enable pnpm
|
||||
RUN corepack enable pnpm
|
||||
|
||||
# Build with placeholder values for NEXT_PUBLIC_* variables.
|
||||
# These are replaced at container startup by docker-entrypoint.js
|
||||
# with real values from the container's environment variables.
|
||||
ARG NEXT_PUBLIC_FASTAPI_BACKEND_URL=__NEXT_PUBLIC_FASTAPI_BACKEND_URL__
|
||||
ARG NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
|
||||
ARG NEXT_PUBLIC_ETL_SERVICE=__NEXT_PUBLIC_ETL_SERVICE__
|
||||
ARG NEXT_PUBLIC_ZERO_CACHE_URL=__NEXT_PUBLIC_ZERO_CACHE_URL__
|
||||
ARG NEXT_PUBLIC_DEPLOYMENT_MODE=__NEXT_PUBLIC_DEPLOYMENT_MODE__
|
||||
|
||||
ENV NEXT_PUBLIC_FASTAPI_BACKEND_URL=$NEXT_PUBLIC_FASTAPI_BACKEND_URL
|
||||
ENV NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=$NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE
|
||||
ENV NEXT_PUBLIC_ETL_SERVICE=$NEXT_PUBLIC_ETL_SERVICE
|
||||
ENV NEXT_PUBLIC_ZERO_CACHE_URL=$NEXT_PUBLIC_ZERO_CACHE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOYMENT_MODE=$NEXT_PUBLIC_DEPLOYMENT_MODE
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
|
|
@ -78,10 +63,6 @@ COPY --from=builder /app/public ./public
|
|||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/app/ ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
# Entrypoint scripts for runtime env var substitution
|
||||
COPY --chown=nextjs:nodejs docker-entrypoint.js ./docker-entrypoint.js
|
||||
COPY --chown=nextjs:nodejs --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
|
@ -91,4 +72,4 @@ ENV PORT=3000
|
|||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
||||
CMD ["node", "server.js"]
|
||||
|
|
@ -7,7 +7,7 @@ import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { SERVER_BACKEND_URL } from "@/lib/env-config";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ model_slug: string }>;
|
||||
|
|
@ -16,7 +16,7 @@ interface PageProps {
|
|||
async function getModel(slug: string): Promise<AnonModel | null> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${BACKEND_URL}/api/v1/public/anon-chat/models/${encodeURIComponent(slug)}`,
|
||||
`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models/${encodeURIComponent(slug)}`,
|
||||
{ next: { revalidate: 300 } }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
|
|
@ -28,7 +28,7 @@ async function getModel(slug: string): Promise<AnonModel | null> {
|
|||
|
||||
async function getAllModels(): Promise<AnonModel[]> {
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
|
||||
const res = await fetch(`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models`, {
|
||||
next: { revalidate: 300 },
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
|
|
@ -136,7 +136,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
|
||||
export async function generateStaticParams() {
|
||||
const models = await getAllModels();
|
||||
return models.filter((m) => m.seo_slug).map((m) => ({ model_slug: m.seo_slug! }));
|
||||
return models.flatMap((m) => (m.seo_slug ? [{ model_slug: m.seo_slug }] : []));
|
||||
}
|
||||
|
||||
export default async function FreeModelPage({ params }: PageProps) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { SERVER_BACKEND_URL } from "@/lib/env-config";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Free AI Chat, No Login Required | SurfSense",
|
||||
|
|
@ -94,7 +94,7 @@ export const metadata: Metadata = {
|
|||
|
||||
async function getModels(): Promise<AnonModel[]> {
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
|
||||
const res = await fetch(`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models`, {
|
||||
next: { revalidate: 300 },
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useTranslations } from "next-intl";
|
|||
import { useState } from "react";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { trackLoginAttempt } from "@/lib/posthog/events";
|
||||
import { AmbientBackground } from "./AmbientBackground";
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ export function GoogleLoginButton() {
|
|||
// cross-origin fetch requests may not be sent on subsequent redirects.
|
||||
// The authorize-redirect endpoint does a server-side redirect to Google
|
||||
// and sets the CSRF cookie properly for same-site context.
|
||||
window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`;
|
||||
window.location.href = buildBackendUrl("/auth/google/authorize-redirect");
|
||||
};
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { useRouter } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
|
||||
import { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { getAuthErrorDetails, isNetworkError } from "@/lib/auth-errors";
|
||||
import { AUTH_TYPE } from "@/lib/env-config";
|
||||
import { ValidationError } from "@/lib/error";
|
||||
import { trackLoginAttempt, trackLoginFailure, trackLoginSuccess } from "@/lib/posthog/events";
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export function LocalLoginForm() {
|
|||
title: null,
|
||||
message: null,
|
||||
});
|
||||
const authType = AUTH_TYPE;
|
||||
const { authType } = useRuntimeConfig();
|
||||
const router = useRouter();
|
||||
const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom);
|
||||
|
||||
|
|
|
|||
5
surfsense_web/app/(home)/login/layout.tsx
Normal file
5
surfsense_web/app/(home)/login/layout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { RuntimeConfig } from "@/components/providers/runtime-config.server";
|
||||
|
||||
export default function LoginLayout({ children }: { children: React.ReactNode }) {
|
||||
return <RuntimeConfig>{children}</RuntimeConfig>;
|
||||
}
|
||||
|
|
@ -6,11 +6,10 @@ import { useTranslations } from "next-intl";
|
|||
import { Suspense, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
|
||||
import { setRedirectPath } from "@/lib/auth-utils";
|
||||
import { AUTH_TYPE } from "@/lib/env-config";
|
||||
import { AmbientBackground } from "./AmbientBackground";
|
||||
import { GoogleLoginButton } from "./GoogleLoginButton";
|
||||
import { LocalLoginForm } from "./LocalLoginForm";
|
||||
|
|
@ -19,8 +18,7 @@ function LoginContent() {
|
|||
const t = useTranslations("auth");
|
||||
const tCommon = useTranslations("common");
|
||||
const router = useRouter();
|
||||
const [authType, setAuthType] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { authType } = useRuntimeConfig();
|
||||
const [urlError, setUrlError] = useState<{ title: string; message: string } | null>(null);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
|
|
@ -96,19 +94,7 @@ function LoginContent() {
|
|||
duration: 4000,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the auth type from centralized config
|
||||
setAuthType(AUTH_TYPE);
|
||||
setIsLoading(false);
|
||||
}, [searchParams, t, tCommon]);
|
||||
|
||||
// Use global loading screen for auth type determination - spinner animation won't reset
|
||||
useGlobalLoadingEffect(isLoading);
|
||||
|
||||
// Show nothing while loading - the GlobalLoadingProvider handles the loading UI
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
}, [router, searchParams, t, tCommon]);
|
||||
|
||||
if (authType === "GOOGLE") {
|
||||
return <GoogleLoginButton />;
|
||||
|
|
|
|||
5
surfsense_web/app/(home)/register/layout.tsx
Normal file
5
surfsense_web/app/(home)/register/layout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { RuntimeConfig } from "@/components/providers/runtime-config.server";
|
||||
|
||||
export default function RegisterLayout({ children }: { children: React.ReactNode }) {
|
||||
return <RuntimeConfig>{children}</RuntimeConfig>;
|
||||
}
|
||||
|
|
@ -9,11 +9,11 @@ import { useEffect, useState } from "react";
|
|||
import { type ExternalToast, toast } from "sonner";
|
||||
import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { AUTH_TYPE } from "@/lib/env-config";
|
||||
import { AppError, ValidationError } from "@/lib/error";
|
||||
import {
|
||||
trackRegistrationAttempt,
|
||||
|
|
@ -25,6 +25,7 @@ import { AmbientBackground } from "../login/AmbientBackground";
|
|||
export default function RegisterPage() {
|
||||
const t = useTranslations("auth");
|
||||
const tCommon = useTranslations("common");
|
||||
const { authType } = useRuntimeConfig();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
|
@ -44,10 +45,10 @@ export default function RegisterPage() {
|
|||
router.replace("/dashboard");
|
||||
return;
|
||||
}
|
||||
if (AUTH_TYPE !== "LOCAL") {
|
||||
if (authType !== "LOCAL") {
|
||||
router.push("/login");
|
||||
}
|
||||
}, [router]);
|
||||
}, [authType, router]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|||
]);
|
||||
|
||||
function getBackendBaseUrl() {
|
||||
const base = process.env.FASTAPI_BACKEND_INTERNAL_URL || "http://localhost:8000";
|
||||
const base =
|
||||
process.env.SURFSENSE_BACKEND_INTERNAL_URL ||
|
||||
// TODO: Remove FASTAPI_BACKEND_INTERNAL_URL after the post-Caddy env migration window.
|
||||
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
|
||||
"http://backend:8000";
|
||||
return base.endsWith("/") ? base.slice(0, -1) : base;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mustGetQuery } from "@rocicorp/zero";
|
||||
import { handleQueryRequest } from "@rocicorp/zero/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { SERVER_BACKEND_URL } from "@/lib/env-config";
|
||||
import type { Context } from "@/types/zero";
|
||||
import { queries } from "@/zero/queries";
|
||||
import { schema } from "@/zero/schema";
|
||||
|
|
@ -11,11 +11,7 @@ import { schema } from "@/zero/schema";
|
|||
// (e.g. http://backend:8000). The browser-facing NEXT_PUBLIC_FASTAPI_BACKEND_URL
|
||||
// (e.g. http://localhost:8929) does NOT resolve from inside the frontend
|
||||
// container and would make every authenticated Zero query fail with a 503.
|
||||
const backendURL = (
|
||||
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
|
||||
process.env.BACKEND_URL ||
|
||||
"http://localhost:8000"
|
||||
).replace(/\/$/, "");
|
||||
const backendURL = SERVER_BACKEND_URL.replace(/\/$/, "");
|
||||
|
||||
async function authenticateRequest(
|
||||
request: Request
|
||||
|
|
|
|||
74
surfsense_web/app/auth/[...path]/route.ts
Normal file
74
surfsense_web/app/auth/[...path]/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import type { NextRequest } from "next/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const HOP_BY_HOP_HEADERS = new Set([
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]);
|
||||
|
||||
function getBackendBaseUrl() {
|
||||
const base =
|
||||
process.env.SURFSENSE_BACKEND_INTERNAL_URL ||
|
||||
// TODO: Remove FASTAPI_BACKEND_INTERNAL_URL after the post-Caddy env migration window.
|
||||
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
|
||||
"http://backend:8000";
|
||||
return base.endsWith("/") ? base.slice(0, -1) : base;
|
||||
}
|
||||
|
||||
function toUpstreamHeaders(headers: Headers) {
|
||||
const nextHeaders = new Headers(headers);
|
||||
nextHeaders.delete("host");
|
||||
nextHeaders.delete("content-length");
|
||||
return nextHeaders;
|
||||
}
|
||||
|
||||
function toClientHeaders(headers: Headers) {
|
||||
const nextHeaders = new Headers(headers);
|
||||
for (const header of HOP_BY_HOP_HEADERS) {
|
||||
nextHeaders.delete(header);
|
||||
}
|
||||
return nextHeaders;
|
||||
}
|
||||
|
||||
async function proxy(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) {
|
||||
const params = await context.params;
|
||||
const path = params.path?.join("/") || "";
|
||||
const upstreamUrl = new URL(`${getBackendBaseUrl()}/auth/${path}`);
|
||||
upstreamUrl.search = request.nextUrl.search;
|
||||
|
||||
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
||||
|
||||
const response = await fetch(upstreamUrl, {
|
||||
method: request.method,
|
||||
headers: toUpstreamHeaders(request.headers),
|
||||
body: hasBody ? request.body : undefined,
|
||||
// `duplex: "half"` is required by the Fetch spec when streaming a
|
||||
// ReadableStream as the request body. Avoids buffering uploads in heap.
|
||||
// @ts-expect-error - `duplex` is not yet in lib.dom RequestInit types.
|
||||
duplex: hasBody ? "half" : undefined,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: toClientHeaders(response.headers),
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
proxy as GET,
|
||||
proxy as POST,
|
||||
proxy as PUT,
|
||||
proxy as PATCH,
|
||||
proxy as DELETE,
|
||||
proxy as OPTIONS,
|
||||
proxy as HEAD,
|
||||
};
|
||||
|
|
@ -16,9 +16,12 @@ export async function GET(
|
|||
};
|
||||
const result = JSON.stringify(payload);
|
||||
|
||||
const redirectUrl = new URL(`/dashboard/${search_space_id}/new-chat`, request.url);
|
||||
|
||||
const response = NextResponse.redirect(redirectUrl, { status: 302 });
|
||||
const response = new NextResponse(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `/dashboard/${search_space_id}/new-chat`,
|
||||
},
|
||||
});
|
||||
response.cookies.set(OAUTH_RESULT_COOKIE, result, {
|
||||
path: "/",
|
||||
maxAge: 60,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ import {
|
|||
extractUserTurnForNewChatApi,
|
||||
type NewChatUserImagePayload,
|
||||
} from "@/lib/chat/user-turn-api-parts";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { NotFoundError } from "@/lib/error";
|
||||
import {
|
||||
trackChatBlocked,
|
||||
|
|
@ -919,10 +919,9 @@ export default function NewChatPage() {
|
|||
if (threadId) {
|
||||
const token = getBearerToken();
|
||||
if (token) {
|
||||
const backendUrl = BACKEND_URL;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${backendUrl}/api/v1/threads/${threadId}/cancel-active-turn`,
|
||||
buildBackendUrl(`/api/v1/threads/${threadId}/cancel-active-turn`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -1110,7 +1109,6 @@ export default function NewChatPage() {
|
|||
let streamBatcher: FrameBatchedUpdater | null = null;
|
||||
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const selection = await getAgentFilesystemSelection(searchSpaceId, {
|
||||
localFilesystemEnabled,
|
||||
});
|
||||
|
|
@ -1147,7 +1145,7 @@ export default function NewChatPage() {
|
|||
}
|
||||
|
||||
const response = await fetchWithTurnCancellingRetry(() =>
|
||||
fetch(`${backendUrl}/api/v1/new_chat`, {
|
||||
fetch(buildBackendUrl("/api/v1/new_chat"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -1642,12 +1640,11 @@ export default function NewChatPage() {
|
|||
}
|
||||
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const selection = await getAgentFilesystemSelection(searchSpaceId, {
|
||||
localFilesystemEnabled,
|
||||
});
|
||||
const response = await fetchWithTurnCancellingRetry(() =>
|
||||
fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
|
||||
fetch(buildBackendUrl(`/api/v1/threads/${resumeThreadId}/resume`), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { RefreshCw, ShieldAlert } from "lucide-react";
|
||||
import { AlertTriangle, RefreshCw, ShieldAlert } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
|
|
@ -19,7 +20,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||
import type { SearchSpace } from "@/contracts/types/search-space.types";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type GatewayConnection = {
|
||||
|
|
@ -39,6 +40,7 @@ type GatewayConnection = {
|
|||
};
|
||||
|
||||
type GatewayConfig = {
|
||||
enabled: boolean;
|
||||
telegram_enabled: boolean;
|
||||
whatsapp_intake_mode: "disabled" | "cloud" | "baileys";
|
||||
slack_enabled: boolean;
|
||||
|
|
@ -47,6 +49,14 @@ type GatewayConfig = {
|
|||
|
||||
type GatewayConfigState = GatewayConfig | null;
|
||||
|
||||
const DISABLED_GATEWAY_CONFIG: GatewayConfig = {
|
||||
enabled: false,
|
||||
telegram_enabled: false,
|
||||
whatsapp_intake_mode: "disabled",
|
||||
slack_enabled: false,
|
||||
discord_enabled: false,
|
||||
};
|
||||
|
||||
type Pairing = {
|
||||
binding_id: number;
|
||||
code: string;
|
||||
|
|
@ -80,16 +90,26 @@ export function MessagingChannelsContent() {
|
|||
const whatsappMode = gatewayConfig?.whatsapp_intake_mode ?? "disabled";
|
||||
const slackGatewayEnabled = gatewayConfig?.slack_enabled ?? false;
|
||||
const discordGatewayEnabled = gatewayConfig?.discord_enabled ?? false;
|
||||
const gatewayDisabled = gatewayConfig?.enabled === false;
|
||||
|
||||
const fetchConnections = useCallback(async (platform?: GatewayPlatform) => {
|
||||
const query = platform ? `?platform=${encodeURIComponent(platform)}` : "";
|
||||
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/connections${query}`);
|
||||
return (await res.json()) as GatewayConnection[];
|
||||
const res = await authenticatedFetch(
|
||||
buildBackendUrl("/api/v1/gateway/connections", platform ? { platform } : undefined)
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? (data as GatewayConnection[]) : [];
|
||||
}, []);
|
||||
|
||||
const fetchGatewayConfig = useCallback(async () => {
|
||||
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/config`);
|
||||
return (await res.json()) as GatewayConfig;
|
||||
const fetchGatewayConfig = useCallback(async (): Promise<GatewayConfig> => {
|
||||
const res = await authenticatedFetch(buildBackendUrl("/api/v1/gateway/config"));
|
||||
if (!res.ok) return DISABLED_GATEWAY_CONFIG;
|
||||
const data = (await res.json()) as Partial<GatewayConfig>;
|
||||
return {
|
||||
...DISABLED_GATEWAY_CONFIG,
|
||||
...data,
|
||||
enabled: data.enabled ?? true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
|
|
@ -125,7 +145,9 @@ export function MessagingChannelsContent() {
|
|||
|
||||
const refreshBaileysHealth = useCallback(async () => {
|
||||
if (whatsappMode !== "baileys") return;
|
||||
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/whatsapp/baileys/health`);
|
||||
const res = await authenticatedFetch(
|
||||
buildBackendUrl("/api/v1/gateway/whatsapp/baileys/health")
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const data = (await res.json()) as BaileysHealth;
|
||||
setBaileysHealth(data);
|
||||
|
|
@ -136,7 +158,7 @@ export function MessagingChannelsContent() {
|
|||
}, [refreshBaileysHealth]);
|
||||
|
||||
async function startPairing(platform: PairingPlatform) {
|
||||
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/start`, {
|
||||
const res = await authenticatedFetch(buildBackendUrl("/api/v1/gateway/bindings/start"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ platform, search_space_id: searchSpaceId }),
|
||||
|
|
@ -148,7 +170,7 @@ export function MessagingChannelsContent() {
|
|||
|
||||
async function installSlackGateway() {
|
||||
const res = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/gateway/slack/install?search_space_id=${searchSpaceId}`
|
||||
buildBackendUrl("/api/v1/gateway/slack/install", { search_space_id: searchSpaceId })
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const data = (await res.json()) as { auth_url?: string };
|
||||
|
|
@ -159,7 +181,7 @@ export function MessagingChannelsContent() {
|
|||
|
||||
async function installDiscordGateway() {
|
||||
const res = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/gateway/discord/install?search_space_id=${searchSpaceId}`
|
||||
buildBackendUrl("/api/v1/gateway/discord/install", { search_space_id: searchSpaceId })
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const data = (await res.json()) as { auth_url?: string };
|
||||
|
|
@ -181,8 +203,8 @@ export function MessagingChannelsContent() {
|
|||
async function revoke(connection: GatewayConnection) {
|
||||
const url =
|
||||
connection.route_type === "account" && connection.account_id
|
||||
? `${BACKEND_URL}/api/v1/gateway/accounts/${connection.account_id}`
|
||||
: `${BACKEND_URL}/api/v1/gateway/bindings/${connection.id}`;
|
||||
? buildBackendUrl(`/api/v1/gateway/accounts/${connection.account_id}`)
|
||||
: buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}`);
|
||||
await authenticatedFetch(url, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
|
@ -205,8 +227,8 @@ export function MessagingChannelsContent() {
|
|||
);
|
||||
const url =
|
||||
connection.route_type === "account" && connection.account_id
|
||||
? `${BACKEND_URL}/api/v1/gateway/accounts/${connection.account_id}/search-space`
|
||||
: `${BACKEND_URL}/api/v1/gateway/bindings/${connection.id}/search-space`;
|
||||
? buildBackendUrl(`/api/v1/gateway/accounts/${connection.account_id}/search-space`)
|
||||
: buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}/search-space`);
|
||||
const res = await authenticatedFetch(url, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -222,7 +244,7 @@ export function MessagingChannelsContent() {
|
|||
}
|
||||
|
||||
async function resume(connection: GatewayConnection) {
|
||||
await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${connection.id}/resume`, {
|
||||
await authenticatedFetch(buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}/resume`), {
|
||||
method: "POST",
|
||||
});
|
||||
await refreshPlatform(connection.platform as GatewayPlatform);
|
||||
|
|
@ -381,7 +403,21 @@ export function MessagingChannelsContent() {
|
|||
<div className="grid items-stretch gap-3 sm:grid-cols-2">
|
||||
{isGatewayConfigLoading ? renderGatewaySkeletons() : null}
|
||||
|
||||
{!isGatewayConfigLoading && !hasEnabledGateway ? (
|
||||
{!isGatewayConfigLoading && gatewayDisabled ? (
|
||||
<Alert className="col-span-full" variant="warning">
|
||||
<AlertTriangle aria-hidden />
|
||||
<AlertTitle>Messaging Channels coming soon</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
Soon you'll be able to connect WhatsApp, Telegram, Slack, and Discord to your
|
||||
SurfSense agent so you can ask questions, route messages to search spaces, and get
|
||||
answers from your knowledge base without leaving your chat app.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isGatewayConfigLoading && !gatewayDisabled && !hasEnabledGateway ? (
|
||||
<Card className="col-span-full border-accent bg-accent/20">
|
||||
<CardHeader className="space-y-1.5 p-4">
|
||||
<CardTitle className="text-sm">No messaging gateways enabled</CardTitle>
|
||||
|
|
@ -389,7 +425,7 @@ export function MessagingChannelsContent() {
|
|||
</Card>
|
||||
) : null}
|
||||
|
||||
{telegramGatewayEnabled ? (
|
||||
{!gatewayDisabled && telegramGatewayEnabled ? (
|
||||
<Card className="order-1 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
|
@ -425,7 +461,7 @@ export function MessagingChannelsContent() {
|
|||
</Card>
|
||||
) : null}
|
||||
|
||||
{slackGatewayEnabled ? (
|
||||
{!gatewayDisabled && slackGatewayEnabled ? (
|
||||
<Card className="order-4 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
|
@ -457,7 +493,7 @@ export function MessagingChannelsContent() {
|
|||
</Card>
|
||||
) : null}
|
||||
|
||||
{discordGatewayEnabled ? (
|
||||
{!gatewayDisabled && discordGatewayEnabled ? (
|
||||
<Card className="order-3 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
|
@ -489,7 +525,7 @@ export function MessagingChannelsContent() {
|
|||
</Card>
|
||||
) : null}
|
||||
|
||||
{whatsappMode !== "disabled" ? (
|
||||
{!gatewayDisabled && whatsappMode !== "disabled" ? (
|
||||
<Card className="order-2 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
|
|
|||
42
surfsense_web/app/dashboard/dashboard-shell.tsx
Normal file
42
surfsense_web/app/dashboard/dashboard-shell.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { ensureTokensFromElectron, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
|
||||
export function DashboardShell({ children }: { children: React.ReactNode }) {
|
||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||
|
||||
// Use the global loading screen - spinner animation won't reset
|
||||
useGlobalLoadingEffect(isCheckingAuth);
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAuth() {
|
||||
let token = getBearerToken();
|
||||
if (!token) {
|
||||
const synced = await ensureTokensFromElectron();
|
||||
if (synced) token = getBearerToken();
|
||||
}
|
||||
if (!token) {
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: [...USER_QUERY_KEY] });
|
||||
setIsCheckingAuth(false);
|
||||
}
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
// Return null while loading - the global provider handles the loading UI
|
||||
if (isCheckingAuth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col ">
|
||||
<div className="flex-1 min-h-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,46 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { ensureTokensFromElectron, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
import { RuntimeConfig } from "@/components/providers/runtime-config.server";
|
||||
import { DashboardShell } from "./dashboard-shell";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||
|
||||
// Use the global loading screen - spinner animation won't reset
|
||||
useGlobalLoadingEffect(isCheckingAuth);
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAuth() {
|
||||
let token = getBearerToken();
|
||||
if (!token) {
|
||||
const synced = await ensureTokensFromElectron();
|
||||
if (synced) token = getBearerToken();
|
||||
}
|
||||
if (!token) {
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: [...USER_QUERY_KEY] });
|
||||
setIsCheckingAuth(false);
|
||||
}
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
// Return null while loading - the global provider handles the loading UI
|
||||
if (isCheckingAuth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col ">
|
||||
<div className="flex-1 min-h-0">{children}</div>
|
||||
</div>
|
||||
<RuntimeConfig>
|
||||
<DashboardShell>{children}</DashboardShell>
|
||||
</RuntimeConfig>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
5
surfsense_web/app/desktop/login/layout.tsx
Normal file
5
surfsense_web/app/desktop/login/layout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { RuntimeConfig } from "@/components/providers/runtime-config.server";
|
||||
|
||||
export default function DesktopLoginLayout({ children }: { children: React.ReactNode }) {
|
||||
return <RuntimeConfig>{children}</RuntimeConfig>;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import { toast } from "sonner";
|
||||
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
|
||||
import { DEFAULT_SHORTCUTS, keyEventToAccelerator } from "@/components/desktop/shortcut-recorder";
|
||||
import { useIsGoogleAuth } from "@/components/providers/runtime-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -17,9 +18,8 @@ import { Spinner } from "@/components/ui/spinner";
|
|||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { setBearerToken } from "@/lib/auth-utils";
|
||||
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const isGoogleAuth = AUTH_TYPE === "GOOGLE";
|
||||
type ShortcutKey = "generalAssist" | "quickAsk" | "screenshotAssist";
|
||||
type ShortcutMap = typeof DEFAULT_SHORTCUTS;
|
||||
|
||||
|
|
@ -189,6 +189,7 @@ function HotkeyRow({
|
|||
export default function DesktopLoginPage() {
|
||||
const router = useRouter();
|
||||
const api = useElectronAPI();
|
||||
const isGoogleAuth = useIsGoogleAuth();
|
||||
const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
|
|
@ -239,7 +240,7 @@ export default function DesktopLoginPage() {
|
|||
const handleGoogleLogin = () => {
|
||||
if (isGoogleRedirecting) return;
|
||||
setIsGoogleRedirecting(true);
|
||||
window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`;
|
||||
window.location.href = buildBackendUrl("/auth/google/authorize-redirect");
|
||||
};
|
||||
|
||||
const autoSetSearchSpace = async () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { loader } from "fumadocs-core/source";
|
|||
import type { MetadataRoute } from "next";
|
||||
import { blog, changelog } from "@/.source/server";
|
||||
import { source as docsSource } from "@/lib/source";
|
||||
import { SERVER_BACKEND_URL } from "@/lib/env-config";
|
||||
|
||||
const blogSource = loader({
|
||||
baseUrl: "/blog",
|
||||
|
|
@ -14,11 +15,10 @@ const changelogSource = loader({
|
|||
});
|
||||
|
||||
const BASE_URL = "https://www.surfsense.com";
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
|
||||
async function getFreeModelSlugs(): Promise<string[]> {
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
|
||||
const res = await fetch(`${SERVER_BACKEND_URL}/api/v1/public/anon-chat/models`, {
|
||||
next: { revalidate: 3600 },
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const backendBaseUrl = (process.env.INTERNAL_FASTAPI_BACKEND_URL || "http://backend:8000").replace(
|
||||
/\/+$/,
|
||||
""
|
||||
);
|
||||
function getBackendBaseUrl() {
|
||||
const base =
|
||||
process.env.SURFSENSE_BACKEND_INTERNAL_URL ||
|
||||
// TODO: Remove FASTAPI_BACKEND_INTERNAL_URL after the post-Caddy env migration window.
|
||||
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
|
||||
"http://backend:8000";
|
||||
return base.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const response = await fetch(`${backendBaseUrl}/verify-token`, {
|
||||
const response = await fetch(`${getBackendBaseUrl()}/verify-token`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: request.headers.get("authorization") || "",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||
import { useApiKey } from "@/hooks/use-api-key";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { getConnectorBenefits } from "../connector-benefits";
|
||||
import type { ConnectFormProps } from "../index";
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
export interface CirclebackConfigProps extends ConnectorConfigProps {
|
||||
onNameChange?: (name: string) => void;
|
||||
|
|
@ -42,17 +42,10 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
|
|||
const doFetch = async () => {
|
||||
if (!connector.search_space_id) return;
|
||||
|
||||
const baseUrl = BACKEND_URL;
|
||||
if (!baseUrl) {
|
||||
console.error("NEXT_PUBLIC_FASTAPI_BACKEND_URL is not configured");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${baseUrl}/api/v1/webhooks/circleback/${connector.search_space_id}/info`,
|
||||
buildBackendUrl(`/api/v1/webhooks/circleback/${connector.search_space_id}/info`),
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
if (controller.signal.aborted) return;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { getReauthEndpoint } from "@/lib/connector-telemetry";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DateRangeSelector } from "../../components/date-range-selector";
|
||||
import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
|
||||
|
|
@ -95,12 +95,13 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
if (!spaceId || !reauthEndpoint) return;
|
||||
setReauthing(true);
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const url = new URL(`${backendUrl}${reauthEndpoint}`);
|
||||
url.searchParams.set("connector_id", String(connector.id));
|
||||
url.searchParams.set("space_id", String(spaceId));
|
||||
url.searchParams.set("return_url", window.location.pathname);
|
||||
const response = await authenticatedFetch(url.toString());
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(reauthEndpoint, {
|
||||
connector_id: connector.id,
|
||||
space_id: spaceId,
|
||||
return_url: window.location.pathname,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
toast.error(data.detail ?? "Failed to initiate re-authentication.");
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|||
import { searchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { OAUTH_RESULT_COOKIE, parseOAuthCallbackResult } from "@/contracts/types/oauth.types";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import {
|
||||
trackConnectorConnected,
|
||||
trackConnectorDeleted,
|
||||
|
|
@ -351,9 +351,7 @@ export const useConnectorDialog = () => {
|
|||
trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click");
|
||||
|
||||
try {
|
||||
// Check if authEndpoint already has query parameters
|
||||
const separator = connector.authEndpoint.includes("?") ? "&" : "?";
|
||||
const url = `${BACKEND_URL}${connector.authEndpoint}${separator}space_id=${searchSpaceId}`;
|
||||
const url = buildBackendUrl(connector.authEndpoint, { space_id: searchSpaceId });
|
||||
|
||||
const response = await authenticatedFetch(url, { method: "GET" });
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { FC } from "react";
|
|||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
import { isSelfHosted } from "@/lib/env-config";
|
||||
import { useIsSelfHosted } from "@/components/providers/runtime-config";
|
||||
import { ConnectorCard } from "../components/connector-card";
|
||||
import {
|
||||
COMPOSIO_CONNECTORS,
|
||||
|
|
@ -22,6 +22,11 @@ type OAuthConnector = (typeof OAUTH_CONNECTORS)[number];
|
|||
type ComposioConnector = (typeof COMPOSIO_CONNECTORS)[number];
|
||||
type OtherConnector = (typeof OTHER_CONNECTORS)[number];
|
||||
type CrawlerConnector = (typeof CRAWLERS)[number];
|
||||
type DeploymentFilterableConnector = {
|
||||
readonly id: string;
|
||||
readonly selfHostedOnly?: boolean;
|
||||
readonly desktopOnly?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the display name from a full connector name.
|
||||
|
|
@ -66,14 +71,14 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
onManage,
|
||||
onViewAccountsList,
|
||||
}) => {
|
||||
const selfHosted = isSelfHosted();
|
||||
const selfHosted = useIsSelfHosted();
|
||||
const { isDesktop } = usePlatform();
|
||||
|
||||
const matchesSearch = (title: string, description: string) =>
|
||||
title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
const passesDeploymentFilter = (c: { selfHostedOnly?: boolean; desktopOnly?: boolean }) =>
|
||||
const passesDeploymentFilter = (c: DeploymentFilterableConnector) =>
|
||||
(!c.selfHostedOnly || selfHosted) && (!c.desktopOnly || isDesktop);
|
||||
|
||||
// Filter connectors based on search and deployment mode
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { getReauthEndpoint } from "@/lib/connector-telemetry";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { formatRelativeDate } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
|
||||
|
|
@ -61,12 +61,13 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
if (!searchSpaceId || !endpoint) return;
|
||||
setReauthingId(connector.id);
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const url = new URL(`${backendUrl}${endpoint}`);
|
||||
url.searchParams.set("connector_id", String(connector.id));
|
||||
url.searchParams.set("space_id", String(searchSpaceId));
|
||||
url.searchParams.set("return_url", window.location.pathname);
|
||||
const response = await authenticatedFetch(url.toString());
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(endpoint, {
|
||||
connector_id: connector.id,
|
||||
space_id: searchSpaceId,
|
||||
return_url: window.location.pathname,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
toast.error(data.detail ?? "Failed to initiate re-authentication.");
|
||||
|
|
|
|||
|
|
@ -1,40 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
|
||||
import { trackLoginAttempt } from "@/lib/posthog/events";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Official Google "G" logo with brand colors
|
||||
const GoogleLogo = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="Google logo"
|
||||
>
|
||||
<title>Google logo</title>
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface SignInButtonProps {
|
||||
/**
|
||||
|
|
@ -46,51 +12,17 @@ interface SignInButtonProps {
|
|||
}
|
||||
|
||||
export const SignInButton = ({ variant = "desktop" }: SignInButtonProps) => {
|
||||
const isGoogleAuth = AUTH_TYPE === "GOOGLE";
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
if (isRedirecting) return;
|
||||
setIsRedirecting(true);
|
||||
trackLoginAttempt("google");
|
||||
window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`;
|
||||
};
|
||||
|
||||
const getClassName = () => {
|
||||
if (variant === "desktop") {
|
||||
return isGoogleAuth
|
||||
? "hidden rounded-full border border-white bg-white px-5 py-2 text-sm font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] md:flex dark:border-white"
|
||||
: "hidden rounded-full bg-black px-8 py-2 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] md:block dark:bg-white dark:text-black";
|
||||
return "hidden rounded-full bg-black px-8 py-2 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] md:block dark:bg-white dark:text-black";
|
||||
}
|
||||
if (variant === "compact") {
|
||||
return isGoogleAuth
|
||||
? "rounded-full border border-white bg-white px-4 py-1.5 text-sm font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-white"
|
||||
: "rounded-full bg-black px-6 py-1.5 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black";
|
||||
return "rounded-full bg-black px-6 py-1.5 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black";
|
||||
}
|
||||
// mobile
|
||||
return isGoogleAuth
|
||||
? "w-full rounded-lg border border-white bg-white px-8 py-2.5 font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-white touch-manipulation"
|
||||
: "w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation";
|
||||
return "w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation";
|
||||
};
|
||||
|
||||
if (isGoogleAuth) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isRedirecting}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-2 transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
getClassName()
|
||||
)}
|
||||
>
|
||||
<GoogleLogo className="h-4 w-4" />
|
||||
<span>Sign In</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href="/login" className={getClassName()}>
|
||||
Sign In
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
interface DownloadOriginalButtonProps {
|
||||
documentId: number;
|
||||
|
|
@ -41,7 +41,7 @@ export function DownloadOriginalButton({ documentId }: DownloadOriginalButtonPro
|
|||
setDownloading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/documents/${documentId}/download-original`,
|
||||
buildBackendUrl(`/api/v1/documents/${documentId}/download-original`),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!response.ok) throw new Error("Download failed");
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { useMediaQuery } from "@/hooks/use-media-query";
|
|||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const PlateEditor = dynamic(
|
||||
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
|
||||
|
|
@ -260,10 +260,12 @@ export function EditorPanelContent({
|
|||
return;
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
const response = await authenticatedFetch(url.toString(), { method: "GET" });
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
|
|
@ -402,7 +404,7 @@ export function EditorPanelContent({
|
|||
return;
|
||||
}
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -496,7 +498,9 @@ export function EditorPanelContent({
|
|||
setDownloading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!response.ok) throw new Error("Download failed");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
export type MemoryScope = "user" | "team";
|
||||
|
||||
|
|
@ -29,10 +30,6 @@ function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
|
|||
return `/api/v1/searchspaces/${searchSpaceId}/memory`;
|
||||
}
|
||||
|
||||
function getBackendUrl(path: string) {
|
||||
return `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${path}`;
|
||||
}
|
||||
|
||||
export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {
|
||||
if (!limits) {
|
||||
return {
|
||||
|
|
@ -65,7 +62,7 @@ export async function fetchMemoryEditorDocument({
|
|||
title?: string | null;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const response = await authenticatedFetch(getBackendUrl(getMemoryPath(scope, searchSpaceId)), {
|
||||
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), {
|
||||
method: "GET",
|
||||
signal,
|
||||
});
|
||||
|
|
@ -97,7 +94,7 @@ export async function saveMemoryMarkdown({
|
|||
searchSpaceId?: number | null;
|
||||
markdown: string;
|
||||
}) {
|
||||
const response = await authenticatedFetch(getBackendUrl(getMemoryPath(scope, searchSpaceId)), {
|
||||
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ memory_md: markdown }),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||
import type { AnonModel, AnonQuotaResponse } from "@/contracts/types/anonymous-chat.types";
|
||||
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
|
||||
import { readSSEStream } from "@/lib/chat/streaming-state";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { trackAnonymousChatMessageSent } from "@/lib/posthog/events";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { QuotaBar } from "./quota-bar";
|
||||
|
|
@ -81,7 +81,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
|
|||
content: m.content,
|
||||
}));
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, {
|
||||
const response = await fetch(buildBackendUrl("/api/v1/public/anon-chat/stream"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import {
|
|||
updateThinkingSteps,
|
||||
updateToolCall,
|
||||
} from "@/lib/chat/streaming-state";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { trackAnonymousChatMessageSent } from "@/lib/posthog/events";
|
||||
import { FreeThread } from "./free-thread";
|
||||
import { RemoveAdsBanner } from "./remove-ads-banner";
|
||||
|
|
@ -176,7 +176,7 @@ export function FreeChatPage() {
|
|||
if (!webSearchEnabled) reqBody.disabled_tools = ["web_search"];
|
||||
if (turnstileToken) reqBody.turnstile_token = turnstileToken;
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, {
|
||||
const response = await fetch(buildBackendUrl("/api/v1/public/anon-chat/stream"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
|
|
|
|||
|
|
@ -37,38 +37,8 @@ import {
|
|||
getAssetLabel,
|
||||
usePrimaryDownload,
|
||||
} from "@/lib/desktop-download-utils";
|
||||
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
|
||||
import { trackLoginAttempt } from "@/lib/posthog/events";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const GoogleLogo = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="Google logo"
|
||||
>
|
||||
<title>Google logo</title>
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
type HeroUseCase = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -314,31 +284,6 @@ export function HeroSection() {
|
|||
}
|
||||
|
||||
function GetStartedButton() {
|
||||
const isGoogleAuth = AUTH_TYPE === "GOOGLE";
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
if (isRedirecting) return;
|
||||
setIsRedirecting(true);
|
||||
trackLoginAttempt("google");
|
||||
window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`;
|
||||
};
|
||||
|
||||
if (isGoogleAuth) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isRedirecting}
|
||||
className="h-14 w-full cursor-pointer gap-3 rounded-lg border border-white bg-white text-center text-base font-medium text-[#1f1f1f] shadow-sm transition duration-150 hover:bg-zinc-100 hover:text-[#1f1f1f] sm:w-56 dark:border-white"
|
||||
>
|
||||
<GoogleLogo className="h-5 w-5" />
|
||||
<span>Continue with Google</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
|
|
|
|||
|
|
@ -72,13 +72,14 @@ import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
|||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useElectronAPI, usePlatform } from "@/hooks/use-platform";
|
||||
import { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { uploadFolderScan } from "@/lib/folder-sync-upload";
|
||||
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
|
||||
import { queries } from "@/zero/queries/index";
|
||||
|
|
@ -226,6 +227,7 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
||||
const platformElectronAPI = useElectronAPI();
|
||||
const electronAPI = desktopFeaturesEnabled ? platformElectronAPI : null;
|
||||
const { etlService } = useRuntimeConfig();
|
||||
const searchSpaceId = Number(params.search_space_id);
|
||||
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
|
||||
const openEditorPanel = useSetAtom(openEditorPanelAtom);
|
||||
|
|
@ -618,7 +620,8 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
folderName: matched.name,
|
||||
searchSpaceId,
|
||||
excludePatterns: matched.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS,
|
||||
fileExtensions: matched.fileExtensions ?? Array.from(getSupportedExtensionsSet()),
|
||||
fileExtensions:
|
||||
matched.fileExtensions ?? Array.from(getSupportedExtensionsSet(undefined, etlService)),
|
||||
rootFolderId: folder.id,
|
||||
});
|
||||
toast.success(`Re-scan complete: ${matched.name}`);
|
||||
|
|
@ -626,7 +629,7 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
toast.error((err as Error)?.message || "Failed to re-scan folder");
|
||||
}
|
||||
},
|
||||
[searchSpaceId, electronAPI]
|
||||
[searchSpaceId, electronAPI, etlService]
|
||||
);
|
||||
|
||||
const handleStopWatching = useCallback(
|
||||
|
|
@ -748,7 +751,9 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
.trim()
|
||||
.slice(0, 80) || "folder";
|
||||
await doExport(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${ctx.folder.id}`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/export`, {
|
||||
folder_id: ctx.folder.id,
|
||||
}),
|
||||
`${safeName}.zip`
|
||||
);
|
||||
toast.success(`Folder "${ctx.folder.name}" exported`);
|
||||
|
|
@ -800,7 +805,9 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
.trim()
|
||||
.slice(0, 80) || "folder";
|
||||
await doExport(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${folder.id}`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/export`, {
|
||||
folder_id: folder.id,
|
||||
}),
|
||||
`${safeName}.zip`
|
||||
);
|
||||
toast.success(`Folder "${folder.name}" exported`);
|
||||
|
|
@ -820,8 +827,8 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
try {
|
||||
const endpoint =
|
||||
doc.document_type === "USER_MEMORY"
|
||||
? `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/users/me/memory`
|
||||
: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory`;
|
||||
? buildBackendUrl("/api/v1/users/me/memory")
|
||||
: buildBackendUrl(`/api/v1/searchspaces/${searchSpaceId}/memory`);
|
||||
const response = await authenticatedFetch(endpoint, { method: "GET" });
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: "Export failed" }));
|
||||
|
|
@ -849,7 +856,9 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${doc.id}/export?format=${format}`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/documents/${doc.id}/export`, {
|
||||
format,
|
||||
}),
|
||||
{ method: "GET" }
|
||||
);
|
||||
|
||||
|
|
@ -1028,8 +1037,8 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
}
|
||||
const endpoint =
|
||||
doc.document_type === "USER_MEMORY"
|
||||
? `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/users/me/memory/reset`
|
||||
: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory/reset`;
|
||||
? buildBackendUrl("/api/v1/users/me/memory/reset")
|
||||
: buildBackendUrl(`/api/v1/searchspaces/${searchSpaceId}/memory/reset`);
|
||||
try {
|
||||
const response = await authenticatedFetch(endpoint, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
|
|
@ -108,10 +108,12 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
}
|
||||
|
||||
try {
|
||||
const url = new URL(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
const response = await authenticatedFetch(url.toString(), { method: "GET" });
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
|
|
@ -165,7 +167,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
setSaving(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -323,7 +325,9 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
setDownloading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!response.ok) throw new Error("Download failed");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,15 @@ import { getBearerToken, handleUnauthorized, refreshAccessToken } from "@/lib/au
|
|||
import { queries } from "@/zero/queries";
|
||||
import { schema } from "@/zero/schema";
|
||||
|
||||
const cacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848";
|
||||
const configuredCacheURL = process.env.NEXT_PUBLIC_ZERO_CACHE_URL;
|
||||
|
||||
function getCacheURL() {
|
||||
if (configuredCacheURL) return configuredCacheURL;
|
||||
if (typeof window !== "undefined") {
|
||||
return `${window.location.origin}/zero`;
|
||||
}
|
||||
return "http://localhost:4848";
|
||||
}
|
||||
|
||||
function ZeroAuthSync() {
|
||||
const zero = useZero();
|
||||
|
|
@ -42,6 +50,7 @@ function ZeroAuthSync() {
|
|||
|
||||
export function ZeroProvider({ children }: { children: React.ReactNode }) {
|
||||
const { data: user } = useAtomValue(currentUserAtom);
|
||||
const cacheURL = useMemo(() => getCacheURL(), []);
|
||||
|
||||
const userId = user?.id;
|
||||
const hasUser = !!userId;
|
||||
|
|
@ -65,7 +74,7 @@ export function ZeroProvider({ children }: { children: React.ReactNode }) {
|
|||
cacheURL,
|
||||
auth,
|
||||
}),
|
||||
[userID, context, auth]
|
||||
[userID, context, cacheURL, auth]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
19
surfsense_web/components/providers/runtime-config.server.tsx
Normal file
19
surfsense_web/components/providers/runtime-config.server.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { connection } from "next/server";
|
||||
import { RuntimeConfigProvider } from "@/components/providers/runtime-config";
|
||||
import {
|
||||
BUILD_TIME_AUTH_TYPE,
|
||||
BUILD_TIME_DEPLOYMENT_MODE,
|
||||
BUILD_TIME_ETL_SERVICE,
|
||||
} from "@/lib/env-config";
|
||||
|
||||
export async function RuntimeConfig({ children }: { children: React.ReactNode }) {
|
||||
await connection();
|
||||
|
||||
const value = {
|
||||
authType: process.env.AUTH_TYPE ?? BUILD_TIME_AUTH_TYPE,
|
||||
etlService: process.env.ETL_SERVICE ?? BUILD_TIME_ETL_SERVICE,
|
||||
deploymentMode: process.env.DEPLOYMENT_MODE ?? BUILD_TIME_DEPLOYMENT_MODE,
|
||||
};
|
||||
|
||||
return <RuntimeConfigProvider value={value}>{children}</RuntimeConfigProvider>;
|
||||
}
|
||||
48
surfsense_web/components/providers/runtime-config.tsx
Normal file
48
surfsense_web/components/providers/runtime-config.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export type AuthType = "LOCAL" | "GOOGLE" | string;
|
||||
export type DeploymentMode = "self-hosted" | "cloud" | string;
|
||||
|
||||
export interface RuntimeConfigValue {
|
||||
authType: AuthType;
|
||||
etlService: string;
|
||||
deploymentMode: DeploymentMode;
|
||||
}
|
||||
|
||||
const RuntimeConfigContext = createContext<RuntimeConfigValue | null>(null);
|
||||
|
||||
export function RuntimeConfigProvider({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: RuntimeConfigValue;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <RuntimeConfigContext.Provider value={value}>{children}</RuntimeConfigContext.Provider>;
|
||||
}
|
||||
|
||||
export function useRuntimeConfig() {
|
||||
const context = useContext(RuntimeConfigContext);
|
||||
if (!context) {
|
||||
throw new Error("useRuntimeConfig must be used within RuntimeConfigProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useIsLocalAuth() {
|
||||
return useRuntimeConfig().authType === "LOCAL";
|
||||
}
|
||||
|
||||
export function useIsGoogleAuth() {
|
||||
return useRuntimeConfig().authType === "GOOGLE";
|
||||
}
|
||||
|
||||
export function useIsSelfHosted() {
|
||||
return useRuntimeConfig().deploymentMode === "self-hosted";
|
||||
}
|
||||
|
||||
export function useIsCloud() {
|
||||
return useRuntimeConfig().deploymentMode === "cloud";
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import { Spinner } from "@/components/ui/spinner";
|
|||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
function ReportPanelSkeleton() {
|
||||
return (
|
||||
|
|
@ -245,7 +245,7 @@ export function ReportPanelContent({
|
|||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/reports/${activeReportId}/export?format=${format}`,
|
||||
buildBackendUrl(`/api/v1/reports/${activeReportId}/export`, { format }),
|
||||
{ method: "GET" }
|
||||
);
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ export function ReportPanelContent({
|
|||
setSaving(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/reports/${activeReportId}/content`,
|
||||
buildBackendUrl(`/api/v1/reports/${activeReportId}/content`),
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -506,7 +506,11 @@ export function ReportPanelContent({
|
|||
</div>
|
||||
) : reportContent.content_type === "typst" ? (
|
||||
<PdfViewer
|
||||
pdfUrl={`${BACKEND_URL}${shareToken ? `/api/v1/public/${shareToken}/reports/${activeReportId}/preview` : `/api/v1/reports/${activeReportId}/preview`}`}
|
||||
pdfUrl={buildBackendUrl(
|
||||
shareToken
|
||||
? `/api/v1/public/${shareToken}/reports/${activeReportId}/preview`
|
||||
: `/api/v1/reports/${activeReportId}/preview`
|
||||
)}
|
||||
isPublic={isPublic}
|
||||
toolbarActions={
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label";
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { Spinner } from "../ui/spinner";
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
|
|||
setIsExporting(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/export`),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } f
|
|||
import { useDropzone } from "react-dropzone";
|
||||
import { toast } from "sonner";
|
||||
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
import { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
|
|
@ -136,6 +137,7 @@ export function DocumentUploadTab({
|
|||
onAccordionStateChange,
|
||||
}: DocumentUploadTabProps) {
|
||||
const t = useTranslations("upload_documents");
|
||||
const { etlService } = useRuntimeConfig();
|
||||
const [files, setFiles] = useState<FileWithId[]>([]);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [accordionValue, setAccordionValue] = useState<string>("");
|
||||
|
|
@ -160,7 +162,7 @@ export function DocumentUploadTab({
|
|||
const electronAPI = useElectronAPI();
|
||||
const isElectron = !!electronAPI?.browseFiles;
|
||||
|
||||
const acceptedFileTypes = useMemo(() => getAcceptedFileTypes(), []);
|
||||
const acceptedFileTypes = useMemo(() => getAcceptedFileTypes(etlService), [etlService]);
|
||||
const supportedExtensions = useMemo(
|
||||
() => getSupportedExtensions(acceptedFileTypes),
|
||||
[acceptedFileTypes]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { X } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -48,6 +49,7 @@ export function FolderWatchDialog({
|
|||
const [submitting, setSubmitting] = useState(false);
|
||||
const [progress, setProgress] = useState<FolderSyncProgress | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const { etlService } = useRuntimeConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (open && initialFolder) {
|
||||
|
|
@ -55,7 +57,10 @@ export function FolderWatchDialog({
|
|||
}
|
||||
}, [open, initialFolder]);
|
||||
|
||||
const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
|
||||
const supportedExtensions = useMemo(
|
||||
() => Array.from(getSupportedExtensionsSet(undefined, etlService)),
|
||||
[etlService]
|
||||
);
|
||||
|
||||
const handleSelectFolder = useCallback(async () => {
|
||||
const api = window.electronAPI;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
import { getAuthHeaders } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
"pdfjs-dist/build/pdf.worker.min.mjs",
|
||||
|
|
@ -223,7 +223,7 @@ function ResumeCard({
|
|||
const previewPath = shareToken
|
||||
? `/api/v1/public/${shareToken}/reports/${reportId}/preview`
|
||||
: `/api/v1/reports/${reportId}/preview`;
|
||||
setPdfUrl(`${BACKEND_URL}${previewPath}`);
|
||||
setPdfUrl(buildBackendUrl(previewPath));
|
||||
|
||||
if (autoOpen && isDesktop && !autoOpenedRef.current) {
|
||||
autoOpenedRef.current = true;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { speakerLabel } from "./schema";
|
||||
|
||||
// Public snapshots predate the transcript.turns shape and keep their own.
|
||||
|
|
@ -121,7 +121,7 @@ export function PodcastPlayer({
|
|||
);
|
||||
} else {
|
||||
const [audioResponse, detail] = await Promise.all([
|
||||
authenticatedFetch(`${BACKEND_URL}/api/v1/podcasts/${podcastId}/stream`, {
|
||||
authenticatedFetch(buildBackendUrl(`/api/v1/podcasts/${podcastId}/stream`), {
|
||||
method: "GET",
|
||||
signal: controller.signal,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -158,7 +158,9 @@ function truncateCommand(command: string, maxLen = 80): string {
|
|||
|
||||
async function downloadSandboxFile(threadId: string, filePath: string, fileName: string) {
|
||||
const token = getBearerToken();
|
||||
const url = `${BACKEND_URL}/api/v1/threads/${threadId}/sandbox/download?path=${encodeURIComponent(filePath)}`;
|
||||
const url = buildBackendUrl(`/api/v1/threads/${threadId}/sandbox/download`, {
|
||||
path: filePath,
|
||||
});
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token || ""}` },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
|
||||
import { FPS } from "@/lib/remotion/constants";
|
||||
import {
|
||||
|
|
@ -137,7 +137,6 @@ function VideoPresentationPlayer({
|
|||
const [isPptxExporting, setIsPptxExporting] = useState(false);
|
||||
const [pptxProgress, setPptxProgress] = useState<string | null>(null);
|
||||
|
||||
const backendUrl = BACKEND_URL ?? "";
|
||||
const audioBlobUrlsRef = useRef<string[]>([]);
|
||||
|
||||
const loadPresentation = useCallback(async () => {
|
||||
|
|
@ -177,7 +176,7 @@ function VideoPresentationPlayer({
|
|||
title: scene.title ?? slide.title,
|
||||
code: scene.code,
|
||||
durationInFrames,
|
||||
audioUrl: slide.audio_url ? `${backendUrl}${slide.audio_url}` : undefined,
|
||||
audioUrl: slide.audio_url ? buildBackendUrl(slide.audio_url) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +221,7 @@ function VideoPresentationPlayer({
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [presentationId, backendUrl, shareToken]);
|
||||
}, [presentationId, shareToken]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPresentation();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ cd SurfSense/docker
|
|||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
This file builds the backend and frontend from your local source code (instead of pulling prebuilt images) and includes pgAdmin for database inspection at [http://localhost:5050](http://localhost:5050). Use the production `docker-compose.yml` for all other cases.
|
||||
This file builds the backend and frontend from your local source code (instead
|
||||
of pulling prebuilt images) and includes pgAdmin for database inspection at
|
||||
[http://localhost:5050](http://localhost:5050). It intentionally keeps raw
|
||||
frontend, backend, and zero-cache ports published for debugging. Use the
|
||||
production `docker-compose.yml` for the default Caddy single-origin setup.
|
||||
|
||||
## Dev-Only Environment Variables
|
||||
|
||||
|
|
@ -22,9 +26,14 @@ The following `.env` variables are **only used by the dev compose file** (they h
|
|||
| `PGADMIN_DEFAULT_EMAIL` | pgAdmin login email | `admin@surfsense.com` |
|
||||
| `PGADMIN_DEFAULT_PASSWORD` | pgAdmin login password | `surfsense` |
|
||||
| `REDIS_PORT` | Exposed Redis port (internal-only in prod) | `6379` |
|
||||
| `NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE` | Frontend build arg for auth type | `LOCAL` |
|
||||
| `NEXT_PUBLIC_ETL_SERVICE` | Frontend build arg for ETL service | `DOCLING` |
|
||||
| `NEXT_PUBLIC_ZERO_CACHE_URL` | Frontend build arg for Zero-cache URL | `http://localhost:4848` |
|
||||
| `NEXT_PUBLIC_DEPLOYMENT_MODE` | Frontend build arg for deployment mode | `self-hosted` |
|
||||
| `AUTH_TYPE` | Runtime auth mode | `LOCAL` |
|
||||
| `ETL_SERVICE` | Runtime document parsing service | `DOCLING` |
|
||||
| `DEPLOYMENT_MODE` | Runtime deployment mode | `self-hosted` |
|
||||
| `ZERO_CACHE_PORT` | Exposed zero-cache port for debugging | `4848` |
|
||||
|
||||
In the production compose file, the `NEXT_PUBLIC_*` frontend variables are automatically derived from `AUTH_TYPE`, `ETL_SERVICE`, and the port settings. In the dev compose file, they are passed as build args since the frontend is built from source.
|
||||
In the production compose file, the frontend reads `AUTH_TYPE`, `ETL_SERVICE`,
|
||||
and `DEPLOYMENT_MODE` at request time. Browser API and Zero traffic are
|
||||
same-origin relative through bundled Caddy.
|
||||
Production Docker exposes only the bundled Caddy proxy by default; dev compose
|
||||
keeps direct service ports so contributors can inspect and restart individual
|
||||
services without going through the proxy.
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ docker compose up -d
|
|||
|
||||
After starting, access SurfSense at:
|
||||
|
||||
- **Frontend**: [http://localhost:3929](http://localhost:3929)
|
||||
- **Backend API**: [http://localhost:8929](http://localhost:8929)
|
||||
- **API Docs**: [http://localhost:8929/docs](http://localhost:8929/docs)
|
||||
- **SurfSense**: [http://localhost:3929](http://localhost:3929)
|
||||
- **Backend API**: [http://localhost:3929/api/v1](http://localhost:3929/api/v1)
|
||||
- **Zero sync**: `ws://localhost:3929/zero`
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
|
@ -99,24 +99,59 @@ docker run -d --name watchtower \
|
|||
|
||||
SurfSense containers are labeled for Watchtower, so `--label-enable` limits updates to the SurfSense services.
|
||||
|
||||
### Ports
|
||||
### Public URL and Ports
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `FRONTEND_PORT` | Frontend service port | `3929` |
|
||||
| `BACKEND_PORT` | Backend API service port | `8929` |
|
||||
| `ZERO_CACHE_PORT` | Zero-cache real-time sync port | `5929` |
|
||||
| `SURFSENSE_PUBLIC_URL` | Public origin used by the frontend, backend OAuth callbacks, and Zero browser URL | `http://localhost:3929` |
|
||||
| `SURFSENSE_SITE_ADDRESS` | Caddy site address. `:80` means local plain HTTP; a hostname enables automatic HTTPS | `:80` |
|
||||
| `LISTEN_HTTP_PORT` | Host port mapped to Caddy's HTTP listener | `3929` |
|
||||
| `LISTEN_HTTPS_PORT` | Host port mapped to Caddy's HTTPS listener for domain mode | `443` |
|
||||
|
||||
### Custom Domain / Reverse Proxy
|
||||
SurfSense includes Caddy by default. The `frontend`, `backend`, and
|
||||
`zero-cache` containers are internal-only in the production compose file; the
|
||||
browser reaches them through Caddy path routing.
|
||||
|
||||
Only set these if serving SurfSense on a real domain via a reverse proxy (Caddy, Nginx, Cloudflare Tunnel, etc.). Leave commented out for standard localhost deployments.
|
||||
### Custom Domain / Automatic HTTPS
|
||||
|
||||
For a real domain, point DNS at the Docker host and set:
|
||||
|
||||
```dotenv
|
||||
SURFSENSE_SITE_ADDRESS=surf.example.com
|
||||
LISTEN_HTTP_PORT=80
|
||||
LISTEN_HTTPS_PORT=443
|
||||
CERT_EMAIL=you@example.com
|
||||
SURFSENSE_PUBLIC_URL=https://surf.example.com
|
||||
```
|
||||
|
||||
Caddy will issue and renew Let's Encrypt certificates automatically. Ports 80
|
||||
and 443 must be reachable from the internet for the default HTTP-01 challenge.
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `NEXT_FRONTEND_URL` | Public frontend URL (e.g. `https://app.yourdomain.com`) |
|
||||
| `BACKEND_URL` | Public backend URL for OAuth callbacks (e.g. `https://api.yourdomain.com`) |
|
||||
| `NEXT_PUBLIC_FASTAPI_BACKEND_URL` | Backend URL used by the frontend (e.g. `https://api.yourdomain.com`) |
|
||||
| `NEXT_PUBLIC_ZERO_CACHE_URL` | Zero-cache URL used by the frontend (e.g. `https://zero.yourdomain.com`) |
|
||||
| `CERT_EMAIL` | Optional ACME contact email |
|
||||
| `CERT_ACME_CA` | ACME directory URL; use Let's Encrypt staging when testing cert issuance |
|
||||
| `CERT_ACME_DNS` | DNS-01 challenge config; requires the custom Caddy build |
|
||||
| `TRUSTED_PROXIES` | CIDR ranges trusted for forwarded client IP headers |
|
||||
| `SURFSENSE_MAX_BODY_SIZE` | Upload limit enforced at the proxy |
|
||||
|
||||
### Bring Your Own Proxy
|
||||
|
||||
If you already run nginx, Traefik, Cloudflare Tunnel, or another ingress, you
|
||||
can comment out the `proxy` service and route traffic to the internal services
|
||||
with the same path contract:
|
||||
|
||||
| Public path | Upstream |
|
||||
|-------------|----------|
|
||||
| `/auth/*` | `backend:8000` |
|
||||
| `/api/v1/*` | `backend:8000` |
|
||||
| `/zero/*` | `zero-cache:4848` |
|
||||
| `/*` | `frontend:3000` |
|
||||
|
||||
Alternative proxies must preserve WebSocket upgrades for `/zero`, avoid
|
||||
buffering streaming responses, allow long-running requests, and support large
|
||||
uploads. For DNS-01 or wildcard certificates with Caddy, build
|
||||
`docker/proxy/Dockerfile` and set `CERT_ACME_DNS` for your DNS provider.
|
||||
|
||||
### Zero-cache (Real-Time Sync)
|
||||
|
||||
|
|
@ -165,7 +200,10 @@ Create credentials at the [Google Cloud Console](https://console.cloud.google.co
|
|||
|
||||
### Connector OAuth Keys
|
||||
|
||||
Uncomment the connectors you want to use. Redirect URIs follow the pattern `http://localhost:8000/api/v1/auth/<connector>/connector/callback`.
|
||||
Uncomment the connectors you want to use. Redirect URIs follow the single-origin
|
||||
pattern `${SURFSENSE_PUBLIC_URL}/api/v1/auth/<connector>/connector/callback`.
|
||||
For local Docker defaults, that means
|
||||
`http://localhost:3929/api/v1/auth/<connector>/connector/callback`.
|
||||
|
||||
| Connector | Variables |
|
||||
|-----------|-----------|
|
||||
|
|
@ -218,6 +256,7 @@ for full setup.
|
|||
|
||||
| Service | Description |
|
||||
|---------|-------------|
|
||||
| `proxy` | Caddy reverse proxy; the only public ingress in production Docker |
|
||||
| `db` | PostgreSQL with pgvector extension |
|
||||
| `migrations` | Short-lived: runs `alembic upgrade head` and verifies `zero_publication`, then exits |
|
||||
| `redis` | Message broker for Celery |
|
||||
|
|
@ -226,7 +265,7 @@ for full setup.
|
|||
| `celery_worker` | Background task processing (document indexing, etc.) |
|
||||
| `celery_beat` | Periodic task scheduler (connector sync) |
|
||||
| `zero-cache` | Rocicorp Zero real-time sync (replicates Postgres to clients) |
|
||||
| `frontend` | Next.js web application |
|
||||
| `frontend` | Next.js web application, internal behind Caddy |
|
||||
|
||||
All services start automatically with `docker compose up -d`.
|
||||
|
||||
|
|
@ -292,9 +331,9 @@ docker compose down -v
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
- **Ports already in use**: Change the relevant `*_PORT` variable in `.env` and restart.
|
||||
- **Port already in use**: Change `LISTEN_HTTP_PORT` in `.env` and restart. In domain mode, use ports `80` and `443` so Caddy can complete certificate issuance.
|
||||
- **Permission errors on Linux**: You may need to prefix `docker` commands with `sudo`.
|
||||
- **Real-time updates not working**: Open DevTools → Console and check for WebSocket errors. Verify `NEXT_PUBLIC_ZERO_CACHE_URL` matches the running zero-cache address.
|
||||
- **Real-time updates not working**: Open DevTools → Console and check for WebSocket errors. In production Docker the expected URL is `${SURFSENSE_PUBLIC_URL}/zero`.
|
||||
- **Line ending issues on Windows**: Run `git config --global core.autocrlf true` before cloning.
|
||||
|
||||
### Migration service exited non-zero
|
||||
|
|
|
|||
|
|
@ -74,7 +74,27 @@ If Watchtower is enabled, it preserves the running image variant tag automatical
|
|||
|
||||
After starting, access SurfSense at:
|
||||
|
||||
- **Frontend**: [http://localhost:3929](http://localhost:3929)
|
||||
- **Backend API**: [http://localhost:8929](http://localhost:8929)
|
||||
- **API Docs**: [http://localhost:8929/docs](http://localhost:8929/docs)
|
||||
- **Zero-cache**: [http://localhost:5929](http://localhost:5929)
|
||||
- **SurfSense**: [http://localhost:3929](http://localhost:3929)
|
||||
- **Backend API**: [http://localhost:3929/api/v1](http://localhost:3929/api/v1)
|
||||
- **Zero sync**: `ws://localhost:3929/zero`
|
||||
|
||||
The installer uses the bundled Caddy reverse proxy by default. The backend and
|
||||
zero-cache containers are not published on separate host ports in the production
|
||||
stack.
|
||||
|
||||
For a custom domain, edit `surfsense/.env` after installation:
|
||||
|
||||
```dotenv
|
||||
SURFSENSE_SITE_ADDRESS=surf.example.com
|
||||
LISTEN_HTTP_PORT=80
|
||||
LISTEN_HTTPS_PORT=443
|
||||
CERT_EMAIL=you@example.com
|
||||
SURFSENSE_PUBLIC_URL=https://surf.example.com
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
cd surfsense
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ zero-cache is included in the Docker Compose setup. The key environment variable
|
|||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `ZERO_CACHE_PORT` | Port for the zero-cache service | `5929` (prod) / `4848` (dev) |
|
||||
| `SURFSENSE_PUBLIC_URL` | Public SurfSense origin used by the browser | `http://localhost:3929` |
|
||||
| `ZERO_ADMIN_PASSWORD` | Password for the zero-cache admin UI and `/statz` endpoint | `surfsense-zero-admin` |
|
||||
| `ZERO_UPSTREAM_DB` | PostgreSQL connection URL for replication | Built from `DB_*` vars |
|
||||
| `NEXT_PUBLIC_ZERO_CACHE_URL` | URL the frontend uses to connect to zero-cache | `http://localhost:<ZERO_CACHE_PORT>` |
|
||||
| `/zero` | Same-origin browser path Caddy routes to zero-cache | `${SURFSENSE_PUBLIC_URL}/zero` |
|
||||
| `ZERO_APP_PUBLICATIONS` | PostgreSQL publication restricting which tables are replicated | `zero_publication` |
|
||||
| `ZERO_NUM_SYNC_WORKERS` | Number of view-sync worker processes. Must be ≤ `ZERO_UPSTREAM_MAX_CONNS` and ≤ `ZERO_CVR_MAX_CONNS` | `4` |
|
||||
| `ZERO_UPSTREAM_MAX_CONNS` | Max connections to upstream PostgreSQL for mutations | `20` |
|
||||
|
|
@ -64,14 +64,18 @@ If running the frontend outside Docker (e.g. `pnpm dev`), you need:
|
|||
```
|
||||
|
||||
Run `uv run alembic upgrade head` from `surfsense_backend/` **before** starting this container so the `zero_publication` exists.
|
||||
2. **`NEXT_PUBLIC_ZERO_CACHE_URL`** set in `surfsense_web/.env` (default: `http://localhost:4848`).
|
||||
2. If the frontend is not behind bundled Caddy, set `NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848` before building/running the frontend so the browser connects directly to zero-cache.
|
||||
3. **`wal_level = logical`** in your PostgreSQL config (see [Manual Installation → Configure PostgreSQL for Zero Sync](/docs/manual-installation#3-configure-postgresql-for-zero-sync)).
|
||||
|
||||
For the full manual setup walkthrough, see the [Manual Installation guide](/docs/manual-installation).
|
||||
|
||||
### Custom Domain / Reverse Proxy
|
||||
|
||||
When deploying behind a reverse proxy, set `NEXT_PUBLIC_ZERO_CACHE_URL` to your public zero-cache URL (e.g., `https://zero.yourdomain.com`). The zero-cache service must be accessible via WebSocket from the browser.
|
||||
The production Docker stack includes Caddy by default. Zero is exposed under the
|
||||
same public origin as the app at `${SURFSENSE_PUBLIC_URL}/zero`, for example
|
||||
`https://surf.example.com/zero`. Zero accepts this single path-component base
|
||||
URL, so Caddy forwards `/zero/*` to the internal `zero-cache:4848` service
|
||||
without stripping the prefix.
|
||||
|
||||
### Database Requirements
|
||||
|
||||
|
|
@ -110,7 +114,7 @@ Zero syncs the following tables for real-time features:
|
|||
|
||||
- **zero-cache not starting**: Check `docker compose logs zero-cache`. Ensure PostgreSQL has `wal_level=logical` (configured in `postgresql.conf`).
|
||||
- **"Insufficient upstream connections" error**: zero-cache defaults `ZERO_NUM_SYNC_WORKERS` to the number of CPU cores, which can exceed connection pool limits on high-core machines. Lower `ZERO_NUM_SYNC_WORKERS` or raise `ZERO_UPSTREAM_MAX_CONNS` / `ZERO_CVR_MAX_CONNS` in your `.env`.
|
||||
- **Frontend not syncing**: Open DevTools → Console and check for WebSocket connection errors. Verify `NEXT_PUBLIC_ZERO_CACHE_URL` matches the running zero-cache address.
|
||||
- **Frontend not syncing**: Open DevTools → Console and check for WebSocket connection errors. In production Docker, verify Caddy serves `${SURFSENSE_PUBLIC_URL}/zero`. In manual local development, verify `NEXT_PUBLIC_ZERO_CACHE_URL` points at the running zero-cache port.
|
||||
- **Stale data after restart**: zero-cache rebuilds its SQLite replica from PostgreSQL on startup. This may take a moment for large databases.
|
||||
|
||||
## Learn More
|
||||
|
|
|
|||
|
|
@ -546,7 +546,10 @@ cd ../docker
|
|||
docker compose -f docker-compose.deps-only.yml up -d
|
||||
```
|
||||
|
||||
The deps-only stack exposes zero-cache on port `4848` by default. Keep `NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848` in your `surfsense_web/.env`.
|
||||
The deps-only stack exposes zero-cache on port `4848` by default. If your
|
||||
frontend is not behind a reverse proxy that serves `/zero`, set
|
||||
`NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848` before building/running the
|
||||
frontend.
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
|
|
@ -577,12 +580,13 @@ Copy-Item -Path .env.example -Destination .env
|
|||
|
||||
Edit the `.env` file and set:
|
||||
|
||||
| ENV VARIABLE | DESCRIPTION |
|
||||
| ------------------------------- | ------------------------------------------- |
|
||||
| NEXT_PUBLIC_FASTAPI_BACKEND_URL | Backend URL (e.g., `http://localhost:8000`) |
|
||||
| NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE | Same value as set in backend AUTH_TYPE i.e `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
|
||||
| NEXT_PUBLIC_ETL_SERVICE | Document parsing service (should match backend ETL_SERVICE): `UNSTRUCTURED`, `LLAMACLOUD`, or `DOCLING` - affects supported file formats in upload interface |
|
||||
| NEXT_PUBLIC_ZERO_CACHE_URL | URL for Zero-cache real-time sync service (e.g., `http://localhost:4848`) |
|
||||
| ENV VARIABLE | DESCRIPTION |
|
||||
| --- | --- |
|
||||
| `SURFSENSE_BACKEND_INTERNAL_URL` | Backend URL used by Next.js server routes, e.g. `http://localhost:8000` or `http://backend:8000` in Docker |
|
||||
| `AUTH_TYPE` | Same value as backend auth type: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
|
||||
| `ETL_SERVICE` | Document parsing service (should match backend ETL_SERVICE): `UNSTRUCTURED`, `LLAMACLOUD`, or `DOCLING`; affects supported file formats in the upload interface |
|
||||
| `DEPLOYMENT_MODE` | `self-hosted` or `cloud`; controls self-hosted-only connector visibility |
|
||||
| `NEXT_PUBLIC_ZERO_CACHE_URL` | Only needed when the browser cannot reach Zero through same-origin `/zero`, e.g. manual local dev at `http://localhost:4848` |
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
|
|
@ -693,7 +697,7 @@ To verify your installation:
|
|||
- **Authentication Problems**: Check your Google OAuth configuration and ensure redirect URIs are set correctly
|
||||
- **LLM Errors**: Confirm your LLM API keys are valid and the selected models are accessible
|
||||
- **File Upload Failures**: Validate your ETL service API key (Unstructured.io or LlamaCloud) or ensure Docling is properly configured
|
||||
- **Real-time updates not working / stale UI**: Verify zero-cache is running (`curl http://localhost:4848/keepalive` returns 200). Open browser DevTools → Console and look for WebSocket errors. Confirm `NEXT_PUBLIC_ZERO_CACHE_URL` in `surfsense_web/.env` matches the running zero-cache address.
|
||||
- **Real-time updates not working / stale UI**: Verify zero-cache is running (`curl http://localhost:4848/keepalive` returns 200). Open browser DevTools → Console and look for WebSocket errors. In default Docker, confirm `/zero` is routed by Caddy. In manual local development, confirm `NEXT_PUBLIC_ZERO_CACHE_URL` in `surfsense_web/.env` matches the running zero-cache address.
|
||||
- **Zero-cache stuck on `Unknown or invalid publications. Specified: [zero_publication]`**: You skipped (or never ran) `uv run alembic upgrade head` from `surfsense_backend/`. Run it, then restart the zero-cache container with `docker restart surfsense-zero-cache`.
|
||||
- **Zero-cache crashes with `_zero.tableMetadata` errors**: A previous run left a half-built SQLite replica behind. Stop the container, remove the volume, and start fresh: `docker rm -f surfsense-zero-cache && docker volume rm surfsense-zero-cache && docker run -d ...` (re-run the command from [Zero-Cache Setup](#zero-cache-setup)).
|
||||
- **`wal_level` is not set to `logical`**: zero-cache requires logical replication. Set `wal_level = logical` in `postgresql.conf`, restart PostgreSQL, and verify with `SHOW wal_level;` in psql.
|
||||
|
|
|
|||
|
|
@ -15,17 +15,19 @@ wired by Compose.
|
|||
## Public URLs
|
||||
|
||||
For localhost-only testing, the defaults are enough for the SurfSense UI, but
|
||||
public webhooks from Telegram, WhatsApp, and Slack require a public HTTPS backend
|
||||
URL. Use your deployed backend URL or a tunnel such as Cloudflare Tunnel or
|
||||
ngrok.
|
||||
public webhooks from Telegram, WhatsApp, and Slack require a public HTTPS
|
||||
SurfSense URL. Use your deployed domain or a tunnel such as Cloudflare Tunnel
|
||||
or ngrok.
|
||||
|
||||
When using a custom domain or tunnel, set:
|
||||
When using a custom domain or tunnel with the bundled Caddy proxy, set:
|
||||
|
||||
```bash
|
||||
BACKEND_URL=https://api.example.com
|
||||
GATEWAY_BASE_URL=https://api.example.com
|
||||
NEXT_FRONTEND_URL=https://app.example.com
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL=https://api.example.com
|
||||
SURFSENSE_PUBLIC_URL=https://surf.example.com
|
||||
SURFSENSE_SITE_ADDRESS=surf.example.com
|
||||
LISTEN_HTTP_PORT=80
|
||||
LISTEN_HTTPS_PORT=443
|
||||
CERT_EMAIL=you@example.com
|
||||
GATEWAY_BASE_URL=https://surf.example.com
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
/**
|
||||
* Runtime environment variable substitution for Next.js Docker images.
|
||||
*
|
||||
* Next.js inlines NEXT_PUBLIC_* values at build time. The Docker image is built
|
||||
* with unique placeholder strings (e.g. __NEXT_PUBLIC_FASTAPI_BACKEND_URL__).
|
||||
* This script replaces those placeholders with real values from the container's
|
||||
* environment variables before the server starts.
|
||||
*
|
||||
* Runs once at container startup via docker-entrypoint.sh.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const replacements = [
|
||||
[
|
||||
"__NEXT_PUBLIC_FASTAPI_BACKEND_URL__",
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000",
|
||||
],
|
||||
[
|
||||
"__NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__",
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "LOCAL",
|
||||
],
|
||||
["__NEXT_PUBLIC_ETL_SERVICE__", process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING"],
|
||||
[
|
||||
"__NEXT_PUBLIC_ZERO_CACHE_URL__",
|
||||
process.env.NEXT_PUBLIC_ZERO_CACHE_URL || "http://localhost:4848",
|
||||
],
|
||||
["__NEXT_PUBLIC_DEPLOYMENT_MODE__", process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted"],
|
||||
];
|
||||
|
||||
let filesProcessed = 0;
|
||||
let filesModified = 0;
|
||||
|
||||
function walk(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.name.endsWith(".js")) {
|
||||
filesProcessed++;
|
||||
let content = fs.readFileSync(full, "utf8");
|
||||
let changed = false;
|
||||
for (const [placeholder, value] of replacements) {
|
||||
if (content.includes(placeholder)) {
|
||||
content = content.replaceAll(placeholder, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
fs.writeFileSync(full, content);
|
||||
filesModified++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[entrypoint] Replacing environment variable placeholders...");
|
||||
for (const [placeholder, value] of replacements) {
|
||||
console.log(` ${placeholder} -> ${value}`);
|
||||
}
|
||||
|
||||
walk(path.join(__dirname, ".next"));
|
||||
|
||||
const serverJs = path.join(__dirname, "server.js");
|
||||
if (fs.existsSync(serverJs)) {
|
||||
let content = fs.readFileSync(serverJs, "utf8");
|
||||
let changed = false;
|
||||
filesProcessed++;
|
||||
for (const [placeholder, value] of replacements) {
|
||||
if (content.includes(placeholder)) {
|
||||
content = content.replaceAll(placeholder, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
fs.writeFileSync(serverJs, content);
|
||||
filesModified++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[entrypoint] Done. Scanned ${filesProcessed} files, modified ${filesModified}.`);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
node /app/docker-entrypoint.js
|
||||
|
||||
exec node server.js
|
||||
|
|
@ -9,6 +9,33 @@ const compat = new FlatCompat({
|
|||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "@/lib/env-config",
|
||||
importNames: ["BACKEND_URL"],
|
||||
message:
|
||||
"Use buildBackendUrl(path, params) for browser-facing backend URLs. BACKEND_URL is empty in proxy mode; importing it bypasses the single URL seam.",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ["**/env-config", "**/env-config.ts"],
|
||||
importNames: ["BACKEND_URL"],
|
||||
message:
|
||||
"Use buildBackendUrl(path, params). Import BACKEND_URL only inside lib/env-config.ts.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
export interface SearchSourceConnector {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -106,16 +106,15 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Build URL with optional search_space_id query parameter
|
||||
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
|
||||
if (spaceId !== undefined) {
|
||||
url.searchParams.append("search_space_id", spaceId.toString());
|
||||
}
|
||||
|
||||
const response = await authenticatedFetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl("/api/v1/search-source-connectors", {
|
||||
search_space_id: spaceId,
|
||||
}),
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch connectors: ${response.statusText}`);
|
||||
|
|
@ -166,15 +165,16 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
spaceId: number
|
||||
) => {
|
||||
try {
|
||||
// Add search_space_id as a query parameter
|
||||
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
|
||||
url.searchParams.append("search_space_id", spaceId.toString());
|
||||
|
||||
const response = await authenticatedFetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(connectorData),
|
||||
});
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl("/api/v1/search-source-connectors", {
|
||||
search_space_id: spaceId,
|
||||
}),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(connectorData),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create connector: ${response.statusText}`);
|
||||
|
|
@ -204,7 +204,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
|
||||
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}`),
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -235,7 +235,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
const deleteConnector = async (connectorId: number) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
|
||||
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}`),
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -267,19 +267,12 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
endDate?: string
|
||||
) => {
|
||||
try {
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams({
|
||||
search_space_id: searchSpaceId.toString(),
|
||||
});
|
||||
if (startDate) {
|
||||
params.append("start_date", startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
params.append("end_date", endDate);
|
||||
}
|
||||
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`,
|
||||
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}/index`, {
|
||||
search_space_id: searchSpaceId,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
}),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
getAnonModelResponse,
|
||||
getAnonModelsResponse,
|
||||
} from "@/contracts/types/anonymous-chat.types";
|
||||
import { BACKEND_URL } from "../env-config";
|
||||
import { buildBackendUrl } from "../env-config";
|
||||
import { ValidationError } from "../error";
|
||||
|
||||
const BASE = "/api/v1/public/anon-chat";
|
||||
|
|
@ -17,14 +17,8 @@ export type AnonUploadResult =
|
|||
| { ok: false; reason: "quota_exceeded" };
|
||||
|
||||
class AnonymousChatApiService {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private fullUrl(path: string): string {
|
||||
return `${this.baseUrl}${BASE}${path}`;
|
||||
return buildBackendUrl(`${BASE}${path}`);
|
||||
}
|
||||
|
||||
getModels = async (): Promise<AnonModel[]> => {
|
||||
|
|
@ -102,4 +96,4 @@ class AnonymousChatApiService {
|
|||
};
|
||||
}
|
||||
|
||||
export const anonymousChatApiService = new AnonymousChatApiService(BACKEND_URL);
|
||||
export const anonymousChatApiService = new AnonymousChatApiService();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ZodType } from "zod";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { getClientPlatform } from "../agent-filesystem";
|
||||
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
|
||||
import {
|
||||
|
|
@ -31,8 +31,6 @@ export type RequestOptions = {
|
|||
};
|
||||
|
||||
class BaseApiService {
|
||||
baseUrl: string;
|
||||
|
||||
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"];
|
||||
|
||||
// Prefixes that don't require auth (checked with startsWith)
|
||||
|
|
@ -44,12 +42,9 @@ class BaseApiService {
|
|||
return typeof window !== "undefined" ? getBearerToken() || "" : "";
|
||||
}
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
// Keep for backward compatibility, but token is now always read from localStorage
|
||||
setBearerToken(_bearerToken: string) {
|
||||
void _bearerToken;
|
||||
// No-op: token is now always read fresh from localStorage via the getter
|
||||
}
|
||||
|
||||
|
|
@ -93,11 +88,6 @@ class BaseApiService {
|
|||
},
|
||||
};
|
||||
|
||||
// Validate the base URL
|
||||
if (!this.baseUrl) {
|
||||
throw new AppError("Base URL is not set.");
|
||||
}
|
||||
|
||||
// Validate the bearer token
|
||||
const isNoAuthEndpoint =
|
||||
this.noAuthEndpoints.includes(url) ||
|
||||
|
|
@ -107,8 +97,7 @@ class BaseApiService {
|
|||
throw new AuthenticationError("You are not authenticated. Please login again.");
|
||||
}
|
||||
|
||||
// Construct the full URL
|
||||
const fullUrl = new URL(url, this.baseUrl).toString();
|
||||
const fullUrl = buildBackendUrl(url);
|
||||
|
||||
// Prepare fetch options
|
||||
const fetchOptions: RequestInit = {
|
||||
|
|
@ -384,7 +373,8 @@ class BaseApiService {
|
|||
options?: Omit<RequestOptions, "method" | "responseType" | "body"> & { body: FormData }
|
||||
) {
|
||||
// Remove Content-Type from options headers if present
|
||||
const { "Content-Type": _, ...headersWithoutContentType } = options?.headers ?? {};
|
||||
const headersWithoutContentType = { ...(options?.headers ?? {}) };
|
||||
delete headersWithoutContentType["Content-Type"];
|
||||
|
||||
return this.request(url, responseSchema, {
|
||||
method: "POST",
|
||||
|
|
@ -399,4 +389,4 @@ class BaseApiService {
|
|||
}
|
||||
}
|
||||
|
||||
export const baseApiService = new BaseApiService(BACKEND_URL);
|
||||
export const baseApiService = new BaseApiService();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Authentication utilities for handling token expiration and redirects
|
||||
*/
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const REDIRECT_PATH_KEY = "surfsense_redirect_path";
|
||||
const BEARER_TOKEN_KEY = "surfsense_bearer_token";
|
||||
|
|
@ -195,7 +195,7 @@ export async function logout(): Promise<boolean> {
|
|||
// Call backend to revoke the refresh token
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/jwt/revoke`, {
|
||||
const response = await fetch(buildBackendUrl("/auth/jwt/revoke"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -273,7 +273,7 @@ export async function refreshAccessToken(): Promise<string | null> {
|
|||
isRefreshing = true;
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/jwt/refresh`, {
|
||||
const response = await fetch(buildBackendUrl("/auth/jwt/refresh"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
// =============================================================================
|
||||
// Types matching backend schemas
|
||||
// =============================================================================
|
||||
|
|
@ -227,5 +227,5 @@ export interface RegenerateParams {
|
|||
* Get the URL for the regenerate endpoint (for streaming fetch)
|
||||
*/
|
||||
export function getRegenerateUrl(threadId: number): string {
|
||||
return `${BACKEND_URL}/api/v1/threads/${threadId}/regenerate`;
|
||||
return buildBackendUrl(`/api/v1/threads/${threadId}/regenerate`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,75 @@
|
|||
/**
|
||||
* Environment configuration for the frontend.
|
||||
*
|
||||
* This file centralizes access to NEXT_PUBLIC_* environment variables.
|
||||
* For Docker deployments, these placeholders are replaced at container startup
|
||||
* via sed in the entrypoint script.
|
||||
*
|
||||
* IMPORTANT: Do not use template literals or complex expressions with these values
|
||||
* as it may prevent the sed replacement from working correctly.
|
||||
* Docker deployments use same-origin relative browser URLs behind Caddy.
|
||||
* NEXT_PUBLIC_* values remain only as build-time fallbacks for packaged clients
|
||||
* like Electron, where there is no bundled Caddy origin.
|
||||
*/
|
||||
|
||||
import packageJson from "../package.json";
|
||||
|
||||
// Auth type: "LOCAL" for email/password, "GOOGLE" for OAuth
|
||||
// Placeholder: __NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
|
||||
export const AUTH_TYPE = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE";
|
||||
// Build-time fallback for packaged clients. Docker runtime reads plain AUTH_TYPE
|
||||
// through the runtime config provider first, then falls back to this baked value.
|
||||
export const BUILD_TIME_AUTH_TYPE = process.env.NEXT_PUBLIC_AUTH_TYPE || "GOOGLE";
|
||||
|
||||
// Backend API URL
|
||||
// Placeholder: __NEXT_PUBLIC_FASTAPI_BACKEND_URL__
|
||||
export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
// Backend API URL. An empty string is valid in proxy mode and means
|
||||
// same-origin relative requests (e.g. /api/v1/... and /auth/...).
|
||||
export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "";
|
||||
|
||||
// ETL Service: "DOCLING", "UNSTRUCTURED", or "LLAMACLOUD"
|
||||
// Placeholder: __NEXT_PUBLIC_ETL_SERVICE__
|
||||
export const ETL_SERVICE = process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING";
|
||||
type BackendUrlParam = string | number | boolean | null | undefined;
|
||||
|
||||
// Deployment Mode: "self-hosted" or "cloud"
|
||||
// Matches backend's SURFSENSE_DEPLOYMENT_MODE - defaults to "self-hosted"
|
||||
// self-hosted: Full access to local file system connectors (Obsidian, etc.)
|
||||
// cloud: Only cloud-based connectors available
|
||||
// Placeholder: __NEXT_PUBLIC_DEPLOYMENT_MODE__
|
||||
export const DEPLOYMENT_MODE = process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted";
|
||||
/**
|
||||
* Build browser-facing backend URLs without breaking proxy mode.
|
||||
*
|
||||
* In proxy mode BACKEND_URL intentionally stays empty, so callers must keep
|
||||
* same-origin relative URLs ("/api/v1/...") and let Caddy route them. When
|
||||
* BACKEND_URL is explicitly configured, the same path resolves against that
|
||||
* absolute backend origin.
|
||||
*/
|
||||
export function buildBackendUrl(
|
||||
path: string,
|
||||
params?: Record<string, BackendUrlParam>
|
||||
): string {
|
||||
const backendPath = path.startsWith("/") ? path : `/${path}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== null && value !== undefined) {
|
||||
queryParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BACKEND_URL) {
|
||||
const url = new URL(backendPath, BACKEND_URL);
|
||||
for (const [key, value] of queryParams) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
if (!queryString) return backendPath;
|
||||
return `${backendPath}${backendPath.includes("?") ? "&" : "?"}${queryString}`;
|
||||
}
|
||||
|
||||
// Server-side backend URL. Relative browser URLs do not work from RSC/API route
|
||||
// code, so server callers should use Docker DNS or an explicit public backend.
|
||||
export const SERVER_BACKEND_URL =
|
||||
process.env.SURFSENSE_BACKEND_INTERNAL_URL ||
|
||||
// TODO: Remove FASTAPI_BACKEND_INTERNAL_URL after the post-Caddy env migration window.
|
||||
process.env.FASTAPI_BACKEND_INTERNAL_URL ||
|
||||
"http://backend:8000";
|
||||
|
||||
// Build-time fallback for packaged clients. Docker runtime reads plain ETL_SERVICE
|
||||
// through the runtime config provider first, then falls back to this baked value.
|
||||
export const BUILD_TIME_ETL_SERVICE = process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING";
|
||||
|
||||
// Build-time fallback for packaged clients. Docker runtime reads plain
|
||||
// DEPLOYMENT_MODE through the runtime config provider first, then falls back to this baked value.
|
||||
export const BUILD_TIME_DEPLOYMENT_MODE = process.env.NEXT_PUBLIC_DEPLOYMENT_MODE || "self-hosted";
|
||||
|
||||
// App version - defaults to package.json version
|
||||
// Can be overridden at build time with NEXT_PUBLIC_APP_VERSION for full git tag version
|
||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || packageJson.version;
|
||||
|
||||
// Helper to check if local auth is enabled
|
||||
export const isLocalAuth = () => AUTH_TYPE === "LOCAL";
|
||||
|
||||
// Helper to check if Google auth is enabled
|
||||
export const isGoogleAuth = () => AUTH_TYPE === "GOOGLE";
|
||||
|
||||
// Helper to check if running in self-hosted mode
|
||||
export const isSelfHosted = () => DEPLOYMENT_MODE === "self-hosted";
|
||||
|
||||
// Helper to check if running in cloud mode
|
||||
export const isCloud = () => DEPLOYMENT_MODE === "cloud";
|
||||
|
|
|
|||
|
|
@ -75,18 +75,21 @@ export const FILE_TYPE_CONFIG: Record<string, Record<string, string[]>> = {
|
|||
},
|
||||
};
|
||||
|
||||
export function getAcceptedFileTypes(): Record<string, string[]> {
|
||||
const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE;
|
||||
export function getAcceptedFileTypes(etlService?: string): Record<string, string[]> {
|
||||
return FILE_TYPE_CONFIG[etlService || "default"] || FILE_TYPE_CONFIG.default;
|
||||
}
|
||||
|
||||
export function getSupportedExtensions(acceptedFileTypes?: Record<string, string[]>): string[] {
|
||||
const types = acceptedFileTypes ?? getAcceptedFileTypes();
|
||||
export function getSupportedExtensions(
|
||||
acceptedFileTypes?: Record<string, string[]>,
|
||||
etlService?: string
|
||||
): string[] {
|
||||
const types = acceptedFileTypes ?? getAcceptedFileTypes(etlService);
|
||||
return Array.from(new Set(Object.values(types).flat())).sort();
|
||||
}
|
||||
|
||||
export function getSupportedExtensionsSet(
|
||||
acceptedFileTypes?: Record<string, string[]>
|
||||
acceptedFileTypes?: Record<string, string[]>,
|
||||
etlService?: string
|
||||
): Set<string> {
|
||||
return new Set(getSupportedExtensions(acceptedFileTypes).map((ext) => ext.toLowerCase()));
|
||||
return new Set(getSupportedExtensions(acceptedFileTypes, etlService).map((ext) => ext.toLowerCase()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@ import { defineConfig, devices } from "@playwright/test";
|
|||
|
||||
const PORT = process.env.PORT || "3000";
|
||||
const BACKEND_PORT = process.env.BACKEND_PORT || "8000";
|
||||
const ZERO_CACHE_PORT = process.env.ZERO_CACHE_PORT || "4848";
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${PORT}`;
|
||||
const useProxyOrigin = process.env.PLAYWRIGHT_USE_PROXY_ORIGIN === "true";
|
||||
const backendURL = useProxyOrigin ? baseURL : `http://localhost:${BACKEND_PORT}`;
|
||||
const zeroCacheURL = useProxyOrigin ? `${baseURL}/zero` : `http://localhost:${ZERO_CACHE_PORT}`;
|
||||
|
||||
process.env.PLAYWRIGHT_TEST_EMAIL ??= "e2e-test@surfsense.net";
|
||||
process.env.PLAYWRIGHT_TEST_PASSWORD ??= "E2eTestPassword123!";
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ??= `http://localhost:${BACKEND_PORT}`;
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE ??= "LOCAL";
|
||||
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ??= backendURL;
|
||||
process.env.SURFSENSE_BACKEND_INTERNAL_URL ??= backendURL;
|
||||
process.env.AUTH_TYPE ??= "LOCAL";
|
||||
process.env.NEXT_PUBLIC_ZERO_CACHE_URL ??= zeroCacheURL;
|
||||
|
||||
/**
|
||||
* Playwright configuration for SurfSense web E2E tests.
|
||||
|
|
@ -67,7 +73,9 @@ export default defineConfig({
|
|||
stderr: "pipe",
|
||||
env: {
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL: process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL,
|
||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE,
|
||||
SURFSENSE_BACKEND_INTERNAL_URL: process.env.SURFSENSE_BACKEND_INTERNAL_URL,
|
||||
AUTH_TYPE: process.env.AUTH_TYPE,
|
||||
NEXT_PUBLIC_ZERO_CACHE_URL: process.env.NEXT_PUBLIC_ZERO_CACHE_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue