feat(sofiia-console): add multi-user team key auth + fix aurora DNS env
- auth.py: adds SOFIIA_CONSOLE_TEAM_KEYS="name:key,..." support;
require_auth now returns identity ("operator"/"user:<name>") for audit;
validate_any_key checks primary + team keys; login sets per-user cookie
- main.py: auth/login+check endpoints return identity field;
imports validate_any_key, _expected_team_cookie_tokens from auth
- docker-compose.node1.yml: adds SOFIIA_CONSOLE_TEAM_KEYS env var;
adds AURORA_SERVICE_URL=http://127.0.0.1:9401 to prevent DNS lookup
failure for aurora-service (not deployed on NODA1)
Made-with: Cursor
This commit is contained in:
@@ -30,6 +30,7 @@ services:
|
|||||||
- GROK_API_KEY=xai-CpoLMPgw91NP9AEdHPhIrvU4ZnhV1q1P8BJBKCpD5kTPFRXJmTOkgGNHwYdZpXMlRxBgHcgcSlIXccxh
|
- GROK_API_KEY=xai-CpoLMPgw91NP9AEdHPhIrvU4ZnhV1q1P8BJBKCpD5kTPFRXJmTOkgGNHwYdZpXMlRxBgHcgcSlIXccxh
|
||||||
- VISION_ENCODER_URL=http://vision-encoder:8001
|
- VISION_ENCODER_URL=http://vision-encoder:8001
|
||||||
- SWAPPER_SERVICE_URL=http://swapper-service:8890
|
- SWAPPER_SERVICE_URL=http://swapper-service:8890
|
||||||
|
- BINANCE_MONITOR_URL=http://dagi-binance-bot-monitor-node1:8893
|
||||||
- IMAGE_GEN_URL=http://swapper-service:8890/image/generate
|
- IMAGE_GEN_URL=http://swapper-service:8890/image/generate
|
||||||
- STT_SERVICE_URL=http://swapper-service:8890
|
- STT_SERVICE_URL=http://swapper-service:8890
|
||||||
- STT_SERVICE_UPLOAD_URL=http://swapper-service:8890/stt
|
- STT_SERVICE_UPLOAD_URL=http://swapper-service:8890/stt
|
||||||
@@ -53,14 +54,20 @@ services:
|
|||||||
- ENABLE_GLOBAL_CAPS_NATS=true
|
- ENABLE_GLOBAL_CAPS_NATS=true
|
||||||
- OLLAMA_URL=http://172.18.0.1:11434
|
- OLLAMA_URL=http://172.18.0.1:11434
|
||||||
- PREFER_NODE_WORKER=true
|
- PREFER_NODE_WORKER=true
|
||||||
|
- ENABLE_CREW_MODEL_ROUTING=1
|
||||||
|
- CREW_SMALL_MODEL=smollm2:135m
|
||||||
|
- CREWAI_WORKER_LLM_PROFILE=crew_local_27b
|
||||||
volumes:
|
volumes:
|
||||||
- ${DEPLOY_ROOT:-.}/services/router/router_config.yaml:/app/router_config.yaml:ro
|
- ${DEPLOY_ROOT:-.}/services/router/router_config.yaml:/app/router_config.yaml:ro
|
||||||
- ${DEPLOY_ROOT:-.}/services/router/router-config.yml:/app/router-config.yml:ro
|
- ${DEPLOY_ROOT:-.}/services/router/router-config.yml:/app/router-config.yml:ro
|
||||||
- ${DEPLOY_ROOT:-.}/config/crewai_agents.json:/config/crewai_agents.json:ro
|
- ${DEPLOY_ROOT:-.}/config/crewai_agents.json:/config/crewai_agents.json:ro
|
||||||
|
- ${DEPLOY_ROOT:-.}/gateway-bot:/app/prompts:ro
|
||||||
- ${DEPLOY_ROOT:-.}/logs:/app/logs
|
- ${DEPLOY_ROOT:-.}/logs:/app/logs
|
||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -78,15 +85,17 @@ services:
|
|||||||
- "8890:8890"
|
- "8890:8890"
|
||||||
- "8891:8891" # Metrics
|
- "8891:8891" # Metrics
|
||||||
environment:
|
environment:
|
||||||
- OLLAMA_BASE_URL=http://172.18.0.1:11434
|
- OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||||
- SWAPPER_CONFIG_PATH=/app/config/swapper_config.yaml
|
- SWAPPER_CONFIG_PATH=/app/config/swapper_config.yaml
|
||||||
- SWAPPER_MODE=single-active
|
- SWAPPER_MODE=single-active
|
||||||
- MAX_CONCURRENT_MODELS=2 # 1 LLM + 1 OCR
|
- MAX_CONCURRENT_MODELS=1
|
||||||
- MODEL_SWAP_TIMEOUT=300
|
- MODEL_SWAP_TIMEOUT=300
|
||||||
- GPU_ENABLED=true
|
- GPU_ENABLED=false
|
||||||
- NODE_ID=node-1-hetzner-gex44
|
- NODE_ID=noda1
|
||||||
- HF_HOME=/root/.cache/huggingface
|
- HF_HOME=/root/.cache/huggingface
|
||||||
- CUDA_VISIBLE_DEVICES=0
|
- CUDA_VISIBLE_DEVICES=
|
||||||
|
- WHISPER_DEVICE=cpu
|
||||||
|
- WHISPER_COMPUTE_TYPE=int8
|
||||||
- CRAWL4AI_URL=http://crawl4ai:11235
|
- CRAWL4AI_URL=http://crawl4ai:11235
|
||||||
# Cloud API keys for video/image generation
|
# Cloud API keys for video/image generation
|
||||||
- GROK_API_KEY=xai-CpoLMPgw91NP9AEdHPhIrvU4ZnhV1q1P8BJBKCpD5kTPFRXJmTOkgGNHwYdZpXMlRxBgHcgcSlIXccxh
|
- GROK_API_KEY=xai-CpoLMPgw91NP9AEdHPhIrvU4ZnhV1q1P8BJBKCpD5kTPFRXJmTOkgGNHwYdZpXMlRxBgHcgcSlIXccxh
|
||||||
@@ -95,19 +104,11 @@ services:
|
|||||||
- ${DEPLOY_ROOT:-.}/services/swapper-service/config/swapper_config_node1.yaml:/app/config/swapper_config.yaml:ro
|
- ${DEPLOY_ROOT:-.}/services/swapper-service/config/swapper_config_node1.yaml:/app/config/swapper_config.yaml:ro
|
||||||
- ${DEPLOY_ROOT:-.}/logs:/app/logs
|
- ${DEPLOY_ROOT:-.}/logs:/app/logs
|
||||||
- swapper-hf-cache-node1:/root/.cache/huggingface
|
- swapper-hf-cache-node1:/root/.cache/huggingface
|
||||||
# GPU support for OCR models
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: 1
|
|
||||||
capabilities: [gpu]
|
|
||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:172.18.0.1"
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8890/health || exit 1"]
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8890/health || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -133,6 +134,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ${DEPLOY_ROOT:-.}/third_party/nature-id:/opt/nature-id:ro
|
- ${DEPLOY_ROOT:-.}/third_party/nature-id:/opt/nature-id:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8085/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8085/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -142,7 +145,7 @@ services:
|
|||||||
|
|
||||||
# Crawl4AI - Advanced Web Crawler with JavaScript support
|
# Crawl4AI - Advanced Web Crawler with JavaScript support
|
||||||
crawl4ai:
|
crawl4ai:
|
||||||
image: unclecode/crawl4ai@sha256:4d8b065bf185962733cb5f9701f4122d03383fa1ab6b5f6a9873f04fa0416a84
|
image: unclecode/crawl4ai:latest
|
||||||
container_name: dagi-crawl4ai-node1
|
container_name: dagi-crawl4ai-node1
|
||||||
ports:
|
ports:
|
||||||
- "11235:11235"
|
- "11235:11235"
|
||||||
@@ -152,6 +155,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -164,10 +169,16 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./gateway-bot
|
context: ./gateway-bot
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
BUILD_SHA: "${BUILD_SHA:-dev}"
|
||||||
|
BUILD_TIME: "${BUILD_TIME:-local}"
|
||||||
container_name: dagi-gateway-node1
|
container_name: dagi-gateway-node1
|
||||||
ports:
|
ports:
|
||||||
- "9300:9300"
|
- "9300:9300"
|
||||||
environment:
|
environment:
|
||||||
|
- BUILD_SHA=${BUILD_SHA:-dev}
|
||||||
|
- BUILD_TIME=${BUILD_TIME:-local}
|
||||||
|
- NODE_ID=NODA1
|
||||||
- ROUTER_URL=${ROUTER_URL:-http://dagi-staging-router:8000}
|
- ROUTER_URL=${ROUTER_URL:-http://dagi-staging-router:8000}
|
||||||
- GATEWAY_MAX_TOKENS_CONCISE=350
|
- GATEWAY_MAX_TOKENS_CONCISE=350
|
||||||
- GATEWAY_MAX_TOKENS_SENPAI_DEFAULT=700
|
- GATEWAY_MAX_TOKENS_SENPAI_DEFAULT=700
|
||||||
@@ -233,6 +244,9 @@ services:
|
|||||||
- CREWAI_SERVICE_URL=http://dagi-staging-crewai-service:9010
|
- CREWAI_SERVICE_URL=http://dagi-staging-crewai-service:9010
|
||||||
- AGROMATRIX_REVIEW_AUTH_MODE=${AGROMATRIX_REVIEW_AUTH_MODE:-bearer}
|
- AGROMATRIX_REVIEW_AUTH_MODE=${AGROMATRIX_REVIEW_AUTH_MODE:-bearer}
|
||||||
- AGROMATRIX_REVIEW_BEARER_TOKENS=${AGROMATRIX_REVIEW_BEARER_TOKENS}
|
- AGROMATRIX_REVIEW_BEARER_TOKENS=${AGROMATRIX_REVIEW_BEARER_TOKENS}
|
||||||
|
- ENABLE_CREW_MODEL_ROUTING=1
|
||||||
|
- CREW_SMALL_MODEL=smollm2:135m
|
||||||
|
- CREWAI_WORKER_LLM_PROFILE=crew_local_27b
|
||||||
# v4.3 FarmOS integration (fail-closed: якщо пусто — агент повідомить "не налаштований")
|
# v4.3 FarmOS integration (fail-closed: якщо пусто — агент повідомить "не налаштований")
|
||||||
- FARMOS_BASE_URL=http://dagi-farmos-node1
|
- FARMOS_BASE_URL=http://dagi-farmos-node1
|
||||||
- FARMOS_TOKEN=${FARMOS_TOKEN:-}
|
- FARMOS_TOKEN=${FARMOS_TOKEN:-}
|
||||||
@@ -251,6 +265,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9300/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9300/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -278,6 +294,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "print(\"ok\")"]
|
test: ["CMD", "python", "-c", "print(\"ok\")"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -324,6 +342,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "print(\"ok\")"]
|
test: ["CMD", "python", "-c", "print(\"ok\")"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -354,6 +374,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "print(\"ok\")"]
|
test: ["CMD", "python", "-c", "print(\"ok\")"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -492,12 +514,14 @@ services:
|
|||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Node Capabilities Service (model inventory for router)
|
# Node Capabilities Service — model inventory + load metrics
|
||||||
node-capabilities:
|
node-capabilities:
|
||||||
build:
|
build:
|
||||||
context: ./services/node-capabilities
|
context: ./services/node-capabilities
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: node-capabilities-node1
|
container_name: node-capabilities-node1
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8099:8099"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ID=noda1
|
- NODE_ID=noda1
|
||||||
- OLLAMA_BASE_URL=http://172.18.0.1:11434
|
- OLLAMA_BASE_URL=http://172.18.0.1:11434
|
||||||
@@ -546,22 +570,31 @@ services:
|
|||||||
|
|
||||||
# NATS (JetStream)
|
# NATS (JetStream)
|
||||||
nats:
|
nats:
|
||||||
image: nats:2.10-alpine
|
image: nats:2.11-alpine
|
||||||
container_name: dagi-nats-node1
|
container_name: dagi-nats-node1
|
||||||
ports:
|
ports:
|
||||||
- "4222:4222"
|
- "4222:4222"
|
||||||
command: ["-js"]
|
- "8222:8222" # HTTP monitoring
|
||||||
|
- "7422:7422" # Leafnode hub (NODA2/NODA3 connect here)
|
||||||
|
command: ["-c", "/etc/nats/nats-node1.conf"]
|
||||||
volumes:
|
volumes:
|
||||||
- nats-data-node1:/data
|
- nats-data-node1:/data
|
||||||
|
- ./config/nats/nats-node1.conf:/etc/nats/nats-node1.conf:ro
|
||||||
networks:
|
networks:
|
||||||
dagi-network:
|
dagi-network:
|
||||||
aliases:
|
aliases:
|
||||||
- nats
|
- nats
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8222/healthz || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
# MinIO Object Storage
|
# MinIO Object Storage
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
|
image: minio/minio:latest
|
||||||
container_name: dagi-minio-node1
|
container_name: dagi-minio-node1
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
@@ -604,6 +637,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9220/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9220/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -627,6 +662,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:9500/health"]
|
test: ["CMD", "wget", "-qO-", "http://localhost:9500/health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -648,9 +685,12 @@ services:
|
|||||||
- MINIO_BUCKET=artifacts
|
- MINIO_BUCKET=artifacts
|
||||||
- MINIO_SECURE=false
|
- MINIO_SECURE=false
|
||||||
depends_on:
|
depends_on:
|
||||||
- nats
|
nats:
|
||||||
- artifact-registry
|
condition: service_healthy
|
||||||
- minio
|
artifact-registry:
|
||||||
|
condition: service_started
|
||||||
|
minio:
|
||||||
|
condition: service_started
|
||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -695,10 +735,14 @@ services:
|
|||||||
- MINIO_SECURE=false
|
- MINIO_SECURE=false
|
||||||
- INDEX_DOC_MAX_BYTES=52428800
|
- INDEX_DOC_MAX_BYTES=52428800
|
||||||
depends_on:
|
depends_on:
|
||||||
- nats
|
nats:
|
||||||
- artifact-registry
|
condition: service_healthy
|
||||||
- rag-service
|
artifact-registry:
|
||||||
- minio
|
condition: service_started
|
||||||
|
rag-service:
|
||||||
|
condition: service_started
|
||||||
|
minio:
|
||||||
|
condition: service_started
|
||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -719,6 +763,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9210/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9210/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -747,6 +793,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9211/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9211/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -773,6 +821,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9212/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9212/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -808,6 +858,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -833,7 +885,7 @@ services:
|
|||||||
|
|
||||||
# Qdrant Vector Database
|
# Qdrant Vector Database
|
||||||
qdrant:
|
qdrant:
|
||||||
image: qdrant/qdrant:v1.7.4
|
image: qdrant/qdrant:v1.13.6
|
||||||
container_name: dagi-qdrant-node1
|
container_name: dagi-qdrant-node1
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
@@ -847,6 +899,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "true"]
|
test: ["CMD", "true"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -855,7 +909,7 @@ services:
|
|||||||
|
|
||||||
# Neo4j Graph Database
|
# Neo4j Graph Database
|
||||||
neo4j:
|
neo4j:
|
||||||
image: neo4j:5.15-community
|
image: neo4j:5.26-community
|
||||||
container_name: dagi-neo4j-node1
|
container_name: dagi-neo4j-node1
|
||||||
ports:
|
ports:
|
||||||
- "7474:7474" # HTTP
|
- "7474:7474" # HTTP
|
||||||
@@ -863,14 +917,17 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NEO4J_AUTH=neo4j/DaarionNeo4j2026!
|
- NEO4J_AUTH=neo4j/DaarionNeo4j2026!
|
||||||
- NEO4J_PLUGINS=["apoc"]
|
- NEO4J_PLUGINS=["apoc"]
|
||||||
- NEO4J_dbms_memory_heap_initial__size=512m
|
- NEO4J_server_memory_heap_initial__size=512m
|
||||||
- NEO4J_dbms_memory_heap_max__size=2G
|
- NEO4J_server_memory_heap_max__size=2G
|
||||||
|
- NEO4J_server_config_strict__validation_enabled=false
|
||||||
volumes:
|
volumes:
|
||||||
- neo4j-data-node1:/data
|
- neo4j-data-node1:/data
|
||||||
- neo4j-logs-node1:/logs
|
- neo4j-logs-node1:/logs
|
||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7474"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7474"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -879,7 +936,7 @@ services:
|
|||||||
|
|
||||||
# Redis Cache
|
# Redis Cache
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:8-alpine
|
||||||
container_name: dagi-redis-node1
|
container_name: dagi-redis-node1
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
@@ -888,6 +945,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "PING"]
|
test: ["CMD", "redis-cli", "PING"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -918,6 +977,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dagi-network
|
- dagi-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8001/health')\""]
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8001/health')\""]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -979,7 +1040,7 @@ services:
|
|||||||
- dagi-network
|
- dagi-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- nats
|
- nats
|
||||||
command: ["run", "--provider", "binance,bybit", "--symbols", "BTCUSDT,ETHUSDT"]
|
command: ["run", "--provider", "binance,bybit", "--symbols", "BTCUSDT,ETHUSDT,BNBUSDT,SOLUSDT,XRPUSDT,ADAUSDT,DOGEUSDT,AVAXUSDT,DOTUSDT,LINKUSDT,POLUSDT,SHIBUSDT,TRXUSDT,UNIUSDT,LTCUSDT,ATOMUSDT,NEARUSDT,ICPUSDT,FILUSDT,APTUSDT,PAXGUSDT"]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD-SHELL
|
- CMD-SHELL
|
||||||
@@ -1084,17 +1145,121 @@ services:
|
|||||||
# Доступний тільки локально; для браузерного setup — SSH tunnel: ssh -L 8088:localhost:8088
|
# Доступний тільки локально; для браузерного setup — SSH tunnel: ssh -L 8088:localhost:8088
|
||||||
- "127.0.0.1:8088:80"
|
- "127.0.0.1:8088:80"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -fsS http://localhost:80 -o /dev/null || exit 1"]
|
# 403 = alive but Drupal requires auth/initial setup; fail-open healthcheck
|
||||||
|
test: ["CMD-SHELL", "curl -sSo /dev/null -w '%{http_code}' http://localhost:80/ | grep -qE '(200|301|302|401|403)' || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
|
||||||
|
|
||||||
|
nats-js-init:
|
||||||
|
image: natsio/nats-box:latest
|
||||||
|
container_name: dagi-nats-js-init-node1
|
||||||
|
depends_on:
|
||||||
|
- nats
|
||||||
|
networks:
|
||||||
|
- dagi-network
|
||||||
|
restart: "no"
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- >-
|
||||||
|
sleep 3 &&
|
||||||
|
(nats --server nats://nats:4222 stream info ATTACHMENTS >/dev/null 2>&1 || nats --server nats://nats:4222 stream add ATTACHMENTS --subjects='attachments.>' --storage=file --retention=limits --max-age=168h --discard=old --replicas=1 --defaults) &&
|
||||||
|
(nats --server nats://nats:4222 stream info TASKS >/dev/null 2>&1 || nats --server nats://nats:4222 stream add TASKS --subjects='tasks.>' --storage=file --retention=limits --max-age=168h --discard=old --replicas=1 --defaults) &&
|
||||||
|
(nats --server nats://nats:4222 stream info MESSAGES >/dev/null 2>&1 || nats --server nats://nats:4222 stream add MESSAGES --subjects='messages.>' --storage=file --retention=limits --max-age=168h --discard=old --replicas=1 --defaults) &&
|
||||||
|
echo JetStream_streams_ready
|
||||||
|
|
||||||
|
# ── Parser Pipeline (NATS ATTACHMENTS consumer → Swapper) ───────────────────
|
||||||
|
parser-pipeline:
|
||||||
|
build:
|
||||||
|
context: ./services/parser-pipeline
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: parser-pipeline
|
||||||
|
environment:
|
||||||
|
- NATS_URL=nats://nats:4222
|
||||||
|
- SWAPPER_URL=http://swapper-service:8890
|
||||||
|
- MEMORY_SERVICE_URL=http://memory-service:8000
|
||||||
|
- COHERE_API_KEY=
|
||||||
|
depends_on:
|
||||||
|
nats:
|
||||||
|
condition: service_healthy
|
||||||
|
swapper-service:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dagi-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ── Ingest Service (HTTP upload → NATS ATTACHMENTS) ─────────────────────────
|
||||||
|
ingest-service:
|
||||||
|
build:
|
||||||
|
context: ./services/ingest-service
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ingest-service
|
||||||
|
ports:
|
||||||
|
- 8100:8100
|
||||||
|
environment:
|
||||||
|
- NATS_URL=nats://nats:4222
|
||||||
|
- SWAPPER_URL=http://swapper-service:8890
|
||||||
|
depends_on:
|
||||||
|
nats:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dagi-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ─── Sofiia Console (Network Control Panel) ────────────────────────────────
|
||||||
|
dagi-sofiia-console-node1:
|
||||||
|
build:
|
||||||
|
context: ./services/sofiia-console
|
||||||
|
args:
|
||||||
|
BUILD_SHA: "${BUILD_SHA:-dev}"
|
||||||
|
BUILD_TIME: "${BUILD_TIME:-local}"
|
||||||
|
container_name: dagi-sofiia-console-node1
|
||||||
|
ports:
|
||||||
|
- "8002:8002"
|
||||||
|
environment:
|
||||||
|
- PORT=8002
|
||||||
|
- ENV=prod
|
||||||
|
- NODE_ID=NODA1
|
||||||
|
- BUILD_SHA=${BUILD_SHA:-dev}
|
||||||
|
- BUILD_TIME=${BUILD_TIME:-local}
|
||||||
|
- SOFIIA_DATA_DIR=/data/sofiia
|
||||||
|
- NODES_REGISTRY_PATH=/config/nodes_registry.yml
|
||||||
|
- NODES_NODA1_SSH_PASSWORD=bRhfV7uNY9m6er
|
||||||
|
- ROUTER_URL=http://dagi-router-node1:8000
|
||||||
|
- GATEWAY_URL=http://dagi-gateway-node1:9300
|
||||||
|
- MEMORY_SERVICE_URL=http://dagi-memory-service-node1:8000
|
||||||
|
- OLLAMA_URL=http://172.18.0.1:11434
|
||||||
|
- SWAPPER_URL=http://swapper-service-node1:8890
|
||||||
|
- XAI_API_KEY=${XAI_API_KEY:-}
|
||||||
|
- GLM5_API_KEY=${GLM5_API_KEY:-}
|
||||||
|
- SOFIIA_CONSOLE_API_KEY=${SOFIIA_CONSOLE_API_KEY:-}
|
||||||
|
- SOFIIA_CONSOLE_TEAM_KEYS=${SOFIIA_CONSOLE_TEAM_KEYS:-}
|
||||||
|
# aurora-service not deployed on NODA1 — set explicit URL to avoid DNS lookup failure
|
||||||
|
- AURORA_SERVICE_URL=http://127.0.0.1:9401
|
||||||
|
volumes:
|
||||||
|
- sofiia-console-data-node1:/data/sofiia
|
||||||
|
- /opt/microdao-daarion/config/nodes_registry.yml:/config/nodes_registry.yml:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8002/api/meta/version', timeout=5)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
networks:
|
||||||
|
- dagi-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
qdrant-data-node1:
|
sofiia-console-data-node1:
|
||||||
name: qdrant-data-node1
|
name: sofiia-console-data-node1
|
||||||
driver: local
|
driver: local
|
||||||
|
qdrant-data-node1:
|
||||||
|
name: microdao-daarion_qdrant-data-node1
|
||||||
|
external: true
|
||||||
neo4j-data-node1:
|
neo4j-data-node1:
|
||||||
name: neo4j-data-node1
|
name: neo4j-data-node1
|
||||||
driver: local
|
driver: local
|
||||||
@@ -1150,6 +1315,7 @@ volumes:
|
|||||||
name: farmos-sites-node1
|
name: farmos-sites-node1
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dagi-network:
|
dagi-network:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ All protected endpoints check:
|
|||||||
1. Cookie "console_token" (browser sessions)
|
1. Cookie "console_token" (browser sessions)
|
||||||
2. X-API-Key header (backward compat: curl, API clients)
|
2. X-API-Key header (backward compat: curl, API clients)
|
||||||
|
|
||||||
|
Single operator key: SOFIIA_CONSOLE_API_KEY
|
||||||
|
Team keys (multi-user): SOFIIA_CONSOLE_TEAM_KEYS = "alice:key1,bob:key2,sergiy:key3"
|
||||||
|
- Each user gets a personal key; login stores a per-user cookie token.
|
||||||
|
- require_auth returns "user:<name>" for audit identification.
|
||||||
|
|
||||||
Dev mode (ENV != prod, no key configured): open access.
|
Dev mode (ENV != prod, no key configured): open access.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
from fastapi import Cookie, HTTPException, Request, Security
|
from fastapi import Cookie, HTTPException, Request, Security
|
||||||
from fastapi.security import APIKeyHeader
|
from fastapi.security import APIKeyHeader
|
||||||
@@ -32,13 +38,46 @@ def get_console_api_key() -> str:
|
|||||||
return os.getenv("SOFIIA_CONSOLE_API_KEY", "").strip()
|
return os.getenv("SOFIIA_CONSOLE_API_KEY", "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_team_keys() -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Parse SOFIIA_CONSOLE_TEAM_KEYS = "alice:key1,bob:key2"
|
||||||
|
Returns {name: key} mapping. Entries with empty name or key are skipped.
|
||||||
|
"""
|
||||||
|
raw = os.getenv("SOFIIA_CONSOLE_TEAM_KEYS", "").strip()
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
result: Dict[str, str] = {}
|
||||||
|
for entry in raw.split(","):
|
||||||
|
entry = entry.strip()
|
||||||
|
if ":" not in entry:
|
||||||
|
continue
|
||||||
|
name, _, key = entry.partition(":")
|
||||||
|
name = name.strip()
|
||||||
|
key = key.strip()
|
||||||
|
if name and key:
|
||||||
|
result[name] = key
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _key_valid(provided: str) -> bool:
|
def _key_valid(provided: str) -> bool:
|
||||||
|
"""Check against the primary key. Returns True if no key configured (open)."""
|
||||||
configured = get_console_api_key()
|
configured = get_console_api_key()
|
||||||
if not configured:
|
if not configured:
|
||||||
return True # no key set → open
|
return True
|
||||||
return secrets.compare_digest(provided.strip(), configured)
|
return secrets.compare_digest(provided.strip(), configured)
|
||||||
|
|
||||||
|
|
||||||
|
def _team_key_identity(provided: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Check provided key against team keys.
|
||||||
|
Returns user name if match found, None otherwise.
|
||||||
|
"""
|
||||||
|
for name, key in _get_team_keys().items():
|
||||||
|
if secrets.compare_digest(provided.strip(), key):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _cookie_token(api_key: str) -> str:
|
def _cookie_token(api_key: str) -> str:
|
||||||
"""Derive a stable session token from the api key (so we never store key directly in cookie)."""
|
"""Derive a stable session token from the api key (so we never store key directly in cookie)."""
|
||||||
return hashlib.sha256(api_key.encode()).hexdigest()
|
return hashlib.sha256(api_key.encode()).hexdigest()
|
||||||
@@ -48,6 +87,30 @@ def _expected_cookie_token() -> str:
|
|||||||
return _cookie_token(get_console_api_key())
|
return _cookie_token(get_console_api_key())
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_team_cookie_tokens() -> Dict[str, str]:
|
||||||
|
"""Returns {cookie_token: user_name} for all team keys."""
|
||||||
|
return {_cookie_token(key): name for name, key in _get_team_keys().items()}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_any_key(provided: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Check provided key against primary key AND team keys.
|
||||||
|
Returns (is_valid, identity_string).
|
||||||
|
identity_string: "operator" for primary key, "user:<name>" for team key.
|
||||||
|
"""
|
||||||
|
# Primary key
|
||||||
|
configured = get_console_api_key()
|
||||||
|
if configured and secrets.compare_digest(provided.strip(), configured):
|
||||||
|
return True, "operator"
|
||||||
|
if not configured:
|
||||||
|
return True, "anonymous"
|
||||||
|
# Team keys
|
||||||
|
name = _team_key_identity(provided)
|
||||||
|
if name:
|
||||||
|
return True, f"user:{name}"
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
|
||||||
def require_auth(
|
def require_auth(
|
||||||
request: Request,
|
request: Request,
|
||||||
x_api_key: str = Security(API_KEY_HEADER),
|
x_api_key: str = Security(API_KEY_HEADER),
|
||||||
@@ -56,6 +119,7 @@ def require_auth(
|
|||||||
Check cookie OR X-API-Key header.
|
Check cookie OR X-API-Key header.
|
||||||
Localhost (127.0.0.1 / ::1) is ALWAYS allowed — no key needed.
|
Localhost (127.0.0.1 / ::1) is ALWAYS allowed — no key needed.
|
||||||
In dev mode without a configured key: pass through.
|
In dev mode without a configured key: pass through.
|
||||||
|
Returns identity string for audit (e.g. "operator", "user:alice", "localhost").
|
||||||
"""
|
"""
|
||||||
# Localhost bypass — always open for local development
|
# Localhost bypass — always open for local development
|
||||||
client_ip = (request.client.host if request.client else "") or ""
|
client_ip = (request.client.host if request.client else "") or ""
|
||||||
@@ -63,19 +127,27 @@ def require_auth(
|
|||||||
return "localhost"
|
return "localhost"
|
||||||
|
|
||||||
configured = get_console_api_key()
|
configured = get_console_api_key()
|
||||||
if not configured:
|
team_keys = _get_team_keys()
|
||||||
|
if not configured and not team_keys:
|
||||||
if _IS_PROD:
|
if _IS_PROD:
|
||||||
logger.warning("SOFIIA_CONSOLE_API_KEY not set in prod — console is OPEN")
|
logger.warning("SOFIIA_CONSOLE_API_KEY not set in prod — console is OPEN")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# 1) Cookie check
|
# 1) Cookie check — primary key cookie
|
||||||
cookie_val = request.cookies.get(_COOKIE_NAME, "")
|
cookie_val = request.cookies.get(_COOKIE_NAME, "")
|
||||||
if cookie_val and secrets.compare_digest(cookie_val, _expected_cookie_token()):
|
if cookie_val:
|
||||||
return "cookie"
|
if configured and secrets.compare_digest(cookie_val, _expected_cookie_token()):
|
||||||
|
return "operator"
|
||||||
|
# Team key cookies
|
||||||
|
team_tokens = _expected_team_cookie_tokens()
|
||||||
|
if cookie_val in team_tokens:
|
||||||
|
return f"user:{team_tokens[cookie_val]}"
|
||||||
|
|
||||||
# 2) X-API-Key header (for API clients / curl)
|
# 2) X-API-Key header
|
||||||
if x_api_key and _key_valid(x_api_key):
|
if x_api_key:
|
||||||
return "header"
|
valid, identity = validate_any_key(x_api_key)
|
||||||
|
if valid:
|
||||||
|
return identity
|
||||||
|
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from .auth import (
|
|||||||
require_api_key, require_api_key_strict, require_auth, require_auth_strict, require_audit_auth,
|
require_api_key, require_api_key_strict, require_auth, require_auth_strict, require_audit_auth,
|
||||||
get_console_api_key, _key_valid, _cookie_token, _expected_cookie_token,
|
get_console_api_key, _key_valid, _cookie_token, _expected_cookie_token,
|
||||||
_COOKIE_NAME, _COOKIE_MAX_AGE, _IS_PROD,
|
_COOKIE_NAME, _COOKIE_MAX_AGE, _IS_PROD,
|
||||||
|
validate_any_key, _expected_team_cookie_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config import (
|
from .config import (
|
||||||
@@ -5114,11 +5115,11 @@ class _LoginBody(BaseModel):
|
|||||||
@app.post("/api/auth/login")
|
@app.post("/api/auth/login")
|
||||||
async def auth_login(body: _LoginBody, response: Response):
|
async def auth_login(body: _LoginBody, response: Response):
|
||||||
"""
|
"""
|
||||||
Verify API key (sent in JSON body — avoids header encoding issues).
|
Verify API key (primary or team key, sent in JSON body).
|
||||||
On success: set httpOnly session cookie, return ok=true.
|
On success: set httpOnly session cookie, return ok=true + identity.
|
||||||
No CORS/header encoding issues since key travels in request body.
|
|
||||||
"""
|
"""
|
||||||
if not _key_valid(body.key):
|
valid, identity = validate_any_key(body.key)
|
||||||
|
if not valid:
|
||||||
raise HTTPException(status_code=401, detail="Invalid key")
|
raise HTTPException(status_code=401, detail="Invalid key")
|
||||||
|
|
||||||
token = _cookie_token(body.key)
|
token = _cookie_token(body.key)
|
||||||
@@ -5131,7 +5132,7 @@ async def auth_login(body: _LoginBody, response: Response):
|
|||||||
max_age=_COOKIE_MAX_AGE,
|
max_age=_COOKIE_MAX_AGE,
|
||||||
path="/",
|
path="/",
|
||||||
)
|
)
|
||||||
return {"ok": True, "auth": "cookie"}
|
return {"ok": True, "auth": "cookie", "identity": identity}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/auth/logout")
|
@app.post("/api/auth/logout")
|
||||||
@@ -5143,19 +5144,23 @@ async def auth_logout(response: Response):
|
|||||||
|
|
||||||
@app.get("/api/auth/check")
|
@app.get("/api/auth/check")
|
||||||
async def auth_check(request: Request):
|
async def auth_check(request: Request):
|
||||||
"""Returns 200 if session is valid, 401 otherwise. Used by UI on startup."""
|
"""Returns 200 + identity if session is valid, 401 otherwise. Used by UI on startup."""
|
||||||
# Localhost is always open — no auth needed
|
import secrets as _sec
|
||||||
client_ip = (request.client.host if request.client else "") or ""
|
client_ip = (request.client.host if request.client else "") or ""
|
||||||
if client_ip in ("127.0.0.1", "::1", "localhost"):
|
if client_ip in ("127.0.0.1", "::1", "localhost"):
|
||||||
return {"ok": True, "auth": "localhost"}
|
return {"ok": True, "auth": "localhost", "identity": "localhost"}
|
||||||
configured = get_console_api_key()
|
configured = get_console_api_key()
|
||||||
if not configured:
|
from .auth import _get_team_keys
|
||||||
return {"ok": True, "auth": "open"}
|
team_keys = _get_team_keys()
|
||||||
from .auth import _expected_cookie_token as _ect
|
if not configured and not team_keys:
|
||||||
|
return {"ok": True, "auth": "open", "identity": "anonymous"}
|
||||||
cookie_val = request.cookies.get(_COOKIE_NAME, "")
|
cookie_val = request.cookies.get(_COOKIE_NAME, "")
|
||||||
import secrets as _sec
|
if cookie_val:
|
||||||
if cookie_val and _sec.compare_digest(cookie_val, _ect()):
|
if configured and _sec.compare_digest(cookie_val, _expected_cookie_token()):
|
||||||
return {"ok": True, "auth": "cookie"}
|
return {"ok": True, "auth": "cookie", "identity": "operator"}
|
||||||
|
team_tokens = _expected_team_cookie_tokens()
|
||||||
|
if cookie_val in team_tokens:
|
||||||
|
return {"ok": True, "auth": "cookie", "identity": f"user:{team_tokens[cookie_val]}"}
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user