diff --git a/.env.example b/.env.example index 2d6c1fe..59373db 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,9 @@ MASTER_VAULT_KEY_V1=base64:qnHTmwYb+uoygIw9MsRMY22vS5YPchY+QOi/E79GAvM= NEO4J_URI_BOLT=bolt://neo4j:7687 NEO4J_USERNAME=neo4j NEO4J_PASSWORD=password -VITE_API_URL=http://127.0.0.1:5001 +# Dev only (vite dev server / docker-compose.yml). Production images use +# same-origin relative URLs proxied by nginx — leave unset for docker-compose.prod.yml. +VITE_API_URL=http://localhost:5001 # Comma-separated CORS allowed origins. Defaults to http://localhost:5173 (Vite dev) when unset. ALLOWED_ORIGINS=http://localhost:5173 DATABASE_URL=postgresql://flowsint:flowsint@localhost:5433/flowsint diff --git a/Makefile b/Makefile index 082f037..9f96966 100644 --- a/Makefile +++ b/Makefile @@ -2,18 +2,17 @@ PROJECT_ROOT := $(shell pwd) COMPOSE_DEV := docker compose -f docker-compose.dev.yml COMPOSE_PROD := docker compose -f docker-compose.prod.yml -COMPOSE_DEPLOY := docker compose -f docker-compose.deploy.yml .PHONY: \ - dev prod deploy \ - build-dev build-prod \ - up-dev up-prod up-deploy down \ + dev prod \ + build-dev \ + up-dev up-prod down \ infra-dev infra-prod infra-stop-dev infra-stop-prod \ migrate-dev migrate-prod \ alembic-upgrade alembic-downgrade alembic-revision \ api frontend celery \ test install clean check-env open-browser-dev open-browser-prod \ - logs-dev logs-prod logs-deploy status \ + logs-dev logs-prod status \ regenerate-router ENV_DIRS := . flowsint-api flowsint-core flowsint-app @@ -63,18 +62,14 @@ open-browser-dev: echo "Frontend ready at http://localhost:5173" prod: - @echo "Starting PROD environment..." + @echo "Starting PROD environment (pre-built images)..." $(MAKE) check-env - $(MAKE) build-prod + $(COMPOSE_PROD) pull $(MAKE) up-prod @echo "" @echo "Production started!" @echo " Frontend: http://localhost:5173" - @echo " API: http://localhost:5001" - -build-prod: - @echo "Building PROD images..." - $(COMPOSE_PROD) build + @echo " API: http://localhost:5173/api (proxied)" up-prod: $(COMPOSE_PROD) up -d @@ -91,27 +86,11 @@ logs-prod: $(COMPOSE_PROD) logs -f open-browser-prod: - @echo "Waiting for frontend on port 80 (Traefik)..." - @bash -c 'until curl -s http://localhost > /dev/null 2>&1; do sleep 2; done' - @open http://localhost 2>/dev/null || \ - xdg-open http://localhost 2>/dev/null || \ - echo "Frontend ready at http://localhost" - -deploy: - @echo "Starting DEPLOY environment (GHCR images)..." - $(MAKE) check-env - $(COMPOSE_DEPLOY) pull - $(COMPOSE_DEPLOY) up -d - @echo "" - @echo "Deploy started!" - @echo " Frontend: http://localhost (via Traefik)" - @echo " API: http://localhost/api" - -up-deploy: - $(COMPOSE_DEPLOY) up -d - -logs-deploy: - $(COMPOSE_DEPLOY) logs -f + @echo "Waiting for frontend on port 5173..." + @bash -c 'until curl -s http://localhost:5173 > /dev/null 2>&1; do sleep 2; done' + @open http://localhost:5173 2>/dev/null || \ + xdg-open http://localhost:5173 2>/dev/null || \ + echo "Frontend ready at http://localhost:5173" migrate-dev: @echo "Running DEV migrations..." @@ -177,7 +156,6 @@ status: down: -$(COMPOSE_DEV) down -$(COMPOSE_PROD) down - -$(COMPOSE_DEPLOY) down clean: @echo "This will remove ALL Docker data. Continue? [y/N]" @@ -185,7 +163,6 @@ clean: if [ "$$confirm" != "y" ]; then exit 1; fi -$(COMPOSE_DEV) down -v --rmi all --remove-orphans -$(COMPOSE_PROD) down -v --rmi all --remove-orphans - -$(COMPOSE_DEPLOY) down -v --rmi all --remove-orphans rm -rf flowsint-app/node_modules rm -rf .venv @@ -203,17 +180,11 @@ help: @echo " make logs-dev - Follow DEV logs" @echo " make infra-dev - Start only infra (postgres/redis/neo4j)" @echo "" - @echo "Production (local build):" - @echo " make prod - Start PROD environment (local build + Traefik)" - @echo " make build-prod - Build PROD images" + @echo "Production (pre-built GHCR images):" + @echo " make prod - Pull images and start PROD environment" @echo " make up-prod - Start PROD containers" @echo " make logs-prod - Follow PROD logs" @echo "" - @echo "Deploy (GHCR images):" - @echo " make deploy - Start with GHCR images (no build)" - @echo " make up-deploy - Start DEPLOY containers" - @echo " make logs-deploy - Follow DEPLOY logs" - @echo "" @echo "Local (no Docker):" @echo " make api - Run API locally" @echo " make frontend - Run frontend locally" diff --git a/README.md b/README.md index d196da2..0f74b89 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,14 @@ copy .env.example flowsint-core\.env copy .env.example flowsint-app\.env ``` -#### 3. Build and start +#### 3. Start ```bat -docker compose -f docker-compose.prod.yml up -d --build +docker compose -f docker-compose.prod.yml up -d ``` +This pulls the pre-built images from GitHub Container Registry — no local build needed. + ### First login Then go to [http://localhost:5173/register](http://localhost:5173/register) and create an account. There are no credentials or account by default. @@ -80,6 +82,40 @@ Then go to [http://localhost:5173/register](http://localhost:5173/register) and > ✅ OSINT investigations need a high level of privacy. Everything is stored on your machine. +### Deploy on a network (team / server) + +The same setup works out of the box on a server: the frontend serves the UI **and** proxies all API calls internally, so no extra configuration is needed for clients. + +```bash +git clone https://github.com/reconurge/flowsint.git +cd flowsint +cp .env.example .env +# Edit .env — see "Before exposing to a network" below +docker compose -f docker-compose.prod.yml up -d +``` + +Anyone on the network can then access Flowsint at `http://:5173`. + +**Before exposing to a network, change the default secrets in `.env`:** + +- `AUTH_SECRET` — signs authentication tokens. Generate one: `openssl rand -hex 32` +- `MASTER_VAULT_KEY_V1` — encrypts stored API keys. Generate one: `python3 -c "import os, base64; print('base64:' + base64.b64encode(os.urandom(32)).decode())"` +- `NEO4J_PASSWORD` — Neo4j database password. + +Only port `5173` is exposed to the network. PostgreSQL, Redis, Neo4j and the API are bound to `127.0.0.1` on the server and reachable only through the frontend proxy. + +To pin a specific version instead of `latest`, set `FLOWSINT_VERSION` in `.env` (e.g. `FLOWSINT_VERSION=1.2.10`). + +**HTTPS (recommended beyond a trusted LAN):** put any reverse proxy in front of port 5173. Example with [Caddy](https://caddyserver.com/): + +``` +flowsint.example.com { + reverse_proxy 127.0.0.1:5173 +} +``` + +When fronting with a reverse proxy, also bind the app port to localhost in `docker-compose.prod.yml` (`"127.0.0.1:5173:8080"`) so clients can only go through HTTPS. + ## What is it? diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3128c5e..2af8199 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -10,7 +10,8 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-flowsint} POSTGRES_DB: ${POSTGRES_DB:-flowsint} ports: - - "5433:5432" + # Bound to localhost: host-only debugging access, never exposed to the network. + - "127.0.0.1:5433:5432" volumes: - pg_data_prod:/var/lib/postgresql/data networks: @@ -26,7 +27,7 @@ services: container_name: flowsint-redis-prod restart: always ports: - - "6379:6379" + - "127.0.0.1:6379:6379" networks: - flowsint_network healthcheck: @@ -40,8 +41,8 @@ services: container_name: flowsint-neo4j-prod restart: always ports: - - "7474:7474" - - "7687:7687" + - "127.0.0.1:7474:7474" + - "127.0.0.1:7687:7687" environment: - NEO4J_AUTH=${NEO4J_USERNAME}/${NEO4J_PASSWORD} - NEO4J_PLUGINS=["apoc"] @@ -62,14 +63,13 @@ services: retries: 10 api: - build: - context: . - dockerfile: flowsint-api/Dockerfile - target: production + image: ghcr.io/reconurge/flowsint-api:${FLOWSINT_VERSION:-latest} container_name: flowsint-api-prod restart: always ports: - - "5001:5001" + # The frontend's nginx proxies /api/ to this service over the Docker + # network — direct access is host-only, for debugging. + - "127.0.0.1:5001:5001" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro environment: @@ -96,10 +96,7 @@ services: - flowsint_network celery: - build: - context: . - dockerfile: flowsint-api/Dockerfile - target: production + image: ghcr.io/reconurge/flowsint-api:${FLOWSINT_VERSION:-latest} container_name: flowsint-celery-prod restart: always command: @@ -144,14 +141,13 @@ services: - flowsint_network app: - build: - context: ./flowsint-app - dockerfile: Dockerfile - args: - - VITE_API_URL=${VITE_API_URL} + # Frontend bundle uses same-origin relative URLs; nginx inside the image + # proxies /api/ to the "api" service. No VITE_API_URL needed. + image: ghcr.io/reconurge/flowsint-app:${FLOWSINT_VERSION:-latest} container_name: flowsint-app-prod restart: always ports: + # The only network-facing port. Serves the UI and proxies /api/ to the API. - "5173:8080" networks: - flowsint_network diff --git a/flowsint-app/Dockerfile b/flowsint-app/Dockerfile index e2473fd..261c884 100644 --- a/flowsint-app/Dockerfile +++ b/flowsint-app/Dockerfile @@ -10,8 +10,9 @@ RUN yarn install --network-timeout 100000 COPY . . -# Build argument for API URL (injected at build time) -ARG VITE_API_URL +# API URL baked at build time. Empty = same-origin relative URLs (/api/...), +# served through the nginx reverse proxy below. Only override for an external API origin. +ARG VITE_API_URL="" ENV VITE_API_URL=${VITE_API_URL} RUN yarn build diff --git a/flowsint-app/nginx.conf b/flowsint-app/nginx.conf index 9df4321..d097697 100644 --- a/flowsint-app/nginx.conf +++ b/flowsint-app/nginx.conf @@ -57,6 +57,26 @@ http { add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # API reverse proxy — same-origin, no CORS needed. + # Expects the API service to be reachable as "api:5001" on the Docker network. + location /api/ { + # Resolve at request time via Docker's embedded DNS: survives api + # container restarts and lets nginx boot before api is up. + resolver 127.0.0.11 valid=10s; + set $api_upstream http://api:5001; + proxy_pass $api_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # SSE support (event/chat streams) + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + } + # Health check endpoint location /health { access_log off; diff --git a/flowsint-app/src/api/api.ts b/flowsint-app/src/api/api.ts index 9538e1a..129baf2 100644 --- a/flowsint-app/src/api/api.ts +++ b/flowsint-app/src/api/api.ts @@ -1,6 +1,6 @@ import { useAuthStore } from '@/stores/auth-store' -const API_URL = import.meta.env.VITE_API_URL +const API_URL = import.meta.env.VITE_API_URL ?? '' export async function fetchWithAuth(endpoint: string, options: RequestInit = {}): Promise { const token = useAuthStore.getState().token diff --git a/flowsint-app/src/api/chat-transport.ts b/flowsint-app/src/api/chat-transport.ts index 57d0094..8152a84 100644 --- a/flowsint-app/src/api/chat-transport.ts +++ b/flowsint-app/src/api/chat-transport.ts @@ -1,7 +1,7 @@ import { DefaultChatTransport } from 'ai' import { useAuthStore } from '@/stores/auth-store' -const API_URL = import.meta.env.VITE_API_URL +const API_URL = import.meta.env.VITE_API_URL ?? '' export function createChatTransport(chatId: string) { return new DefaultChatTransport({ diff --git a/flowsint-app/src/hooks/use-events.ts b/flowsint-app/src/hooks/use-events.ts index e418169..ab7d8aa 100644 --- a/flowsint-app/src/hooks/use-events.ts +++ b/flowsint-app/src/hooks/use-events.ts @@ -5,7 +5,7 @@ import { queryKeys } from '@/api/query-keys' import { useAuthStore } from '@/stores/auth-store' import { connectSSE } from '@/api/sse' -const API_URL = import.meta.env.VITE_API_URL +const API_URL = import.meta.env.VITE_API_URL ?? '' export function useEvents(sketch_id: string | undefined) { diff --git a/flowsint-app/src/hooks/use-graph-refresh.ts b/flowsint-app/src/hooks/use-graph-refresh.ts index 688375f..32049a0 100644 --- a/flowsint-app/src/hooks/use-graph-refresh.ts +++ b/flowsint-app/src/hooks/use-graph-refresh.ts @@ -4,7 +4,7 @@ import { useAuthStore } from '@/stores/auth-store' import { EventLevel } from '@/types' import { connectSSE } from '@/api/sse' -const API_URL = import.meta.env.VITE_API_URL +const API_URL = import.meta.env.VITE_API_URL ?? '' export function useGraphRefresh(sketch_id: string | undefined) { const refetchGraph = useGraphControls((s) => s.refetchGraph)