From 612d5801dee18941456e1158d1ba0377a8d88d0c Mon Sep 17 00:00:00 2001 From: Luis Erlacher Date: Fri, 10 Oct 2025 14:30:15 -0300 Subject: [PATCH 1/3] feat: Enhance Playwright and MCP configuration for Kubernetes deployment - Updated docker-compose.yml to include PLAYWRIGHT_BROWSERS_PATH and MCP_PUBLIC_URL environment variables. - Modified k8s-manifests-complete.yaml to add Playwright and MCP configurations in the ConfigMap and deployment spec. - Adjusted resource limits in k8s manifests for improved performance during crawling. - Updated Dockerfiles to install Playwright browsers in accessible locations for appuser. - Added HTTP health check endpoint in mcp_server.py for better monitoring. - Enhanced MCP API to utilize MCP_PUBLIC_URL for generating client configuration. - Created MCP_PUBLIC_URL_GUIDE.md for detailed configuration instructions. - Documented changes and recommendations in K8S_COMPLETE_ADJUSTMENTS.md. --- .env.example | 11 + CLAUDE.md | 65 ++ K8S_COMPLETE_ADJUSTMENTS.md | 826 ++++++++++++++++++++++++ MCP_PUBLIC_URL_GUIDE.md | 452 +++++++++++++ docker-compose.yml | 2 + k8s-manifests-complete.yaml | 46 +- python/Dockerfile.k8s.server | 9 +- python/Dockerfile.server | 8 +- python/src/mcp_server/mcp_server.py | 28 +- python/src/server/api_routes/mcp_api.py | 33 +- 10 files changed, 1462 insertions(+), 18 deletions(-) create mode 100644 K8S_COMPLETE_ADJUSTMENTS.md create mode 100644 MCP_PUBLIC_URL_GUIDE.md diff --git a/.env.example b/.env.example index 9647c8f..363b424 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,17 @@ ARCHON_DOCS_PORT=3838 # If not set, defaults to localhost, 127.0.0.1, ::1, and the HOST value above VITE_ALLOWED_HOSTS= +# MCP Public URL Configuration +# This is the publicly accessible URL for the MCP server +# Used to generate client configuration JSON for Claude Code, Cursor, etc. +# Format: "domain.com:8051" or "localhost:8051" +# Examples: +# - Development: localhost:8051 (default) +# - Production: archon.automatizase.com.br:8051 +# - Custom domain: mcp.mycompany.com:8051 +# If not set, defaults to "localhost:8051" +MCP_PUBLIC_URL=localhost:8051 + # Development Tools # VITE_SHOW_DEVTOOLS: Show TanStack Query DevTools (for developers only) # Set to "true" to enable the DevTools panel in bottom right corner diff --git a/CLAUDE.md b/CLAUDE.md index 77673db..61670f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,71 @@ make test-fe # Frontend tests only make test-be # Backend tests only ``` +### Kubernetes Deployment + +**Build and push images to registry:** + +```bash +# Navigate to project root +cd /home/lperl/Archon + +# Build and push Server image (includes Playwright and MCP_PUBLIC_URL fixes) +docker build -f python/Dockerfile.k8s.server \ + -t git.automatizase.com.br/luis.erlacher/archon/server:k8s-latest \ + python/ +docker push git.automatizase.com.br/luis.erlacher/archon/server:k8s-latest + +# Build and push MCP image +docker build -f python/Dockerfile.k8s.mcp \ + -t git.automatizase.com.br/luis.erlacher/archon/mcp:k8s-latest \ + python/ +docker push git.automatizase.com.br/luis.erlacher/archon/mcp:k8s-latest + +# Build and push Frontend image (if needed) +docker build -f archon-ui-main/Dockerfile.k8s.production \ + -t git.automatizase.com.br/luis.erlacher/archon/frontend:k8s-latest \ + archon-ui-main/ +docker push git.automatizase.com.br/luis.erlacher/archon/frontend:k8s-latest + +# Optional: Build and push Agents image +docker build -f python/Dockerfile.k8s.agents \ + -t git.automatizase.com.br/luis.erlacher/archon/agents:k8s-latest \ + python/ +docker push git.automatizase.com.br/luis.erlacher/archon/agents:k8s-latest +``` + +**Deploy to Kubernetes:** + +```bash +# IMPORTANT: First, update ConfigMap with your domain! +# Edit k8s-manifests-complete.yaml line 61: +# MCP_PUBLIC_URL: "your-domain.com:8051" # ← CHANGE THIS! + +# Apply manifests +kubectl apply -f k8s-manifests-complete.yaml + +# Restart deployments to use new images +kubectl rollout restart deployment/archon-server -n archon +kubectl rollout restart deployment/archon-mcp -n archon +kubectl rollout restart deployment/archon-frontend -n archon + +# Monitor deployment status +kubectl rollout status deployment/archon-server -n archon +kubectl get pods -n archon -w + +# View logs +kubectl logs -f deployment/archon-server -n archon +kubectl logs -f deployment/archon-mcp -n archon + +# Verify configuration +kubectl get configmap archon-config -n archon -o yaml | grep -A 2 MCP_PUBLIC_URL +``` + +**Complete K8s documentation:** +- Full deployment guide: `K8S_COMPLETE_ADJUSTMENTS.md` +- MCP Public URL configuration: `MCP_PUBLIC_URL_GUIDE.md` +- Image naming convention: `service:k8s-latest` (e.g., `server:k8s-latest`, `mcp:k8s-latest`) + ## Architecture Overview @PRPs/ai_docs/ARCHITECTURE.md diff --git a/K8S_COMPLETE_ADJUSTMENTS.md b/K8S_COMPLETE_ADJUSTMENTS.md new file mode 100644 index 0000000..a8c4493 --- /dev/null +++ b/K8S_COMPLETE_ADJUSTMENTS.md @@ -0,0 +1,826 @@ +# Kubernetes Complete Adjustments Guide + +## Executive Summary + +Este documento descreve **todas as mudanças necessárias** para executar o Archon em produção no Kubernetes, não apenas o Playwright. As mudanças cobrem: + +- ✅ Playwright browser binaries (JÁ CORRIGIDO) +- ⚠️ Variáveis de ambiente em K8s manifests +- ⚠️ Resource limits para crawling +- ⚠️ Nginx permissions e configuration +- ⚠️ Security contexts avançados +- ⚠️ Health checks otimizados +- ⚠️ Init containers para warm-up + +--- + +## 1. Playwright Browser Binaries (✅ JÁ CORRIGIDO) + +### Problema Identificado +Playwright instalava binários em `/root/.cache/ms-playwright` (root), mas container roda como `appuser` (UID 1001) e não tinha acesso. + +### Solução Aplicada + +**Dockerfile.k8s.server:** +```dockerfile +# Install Playwright browsers in a location accessible to appuser +ENV PATH=/venv/bin:$PATH +ENV PLAYWRIGHT_BROWSERS_PATH=/app/ms-playwright +RUN mkdir -p /app/ms-playwright && \ + playwright install chromium && \ + chown -R appuser:appuser /app/ms-playwright + +# Runtime environment +ENV PLAYWRIGHT_BROWSERS_PATH=/app/ms-playwright +``` + +**Dockerfile.server (Docker Compose):** +```dockerfile +ENV PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright +RUN mkdir -p /tmp/ms-playwright && \ + playwright install chromium && \ + chmod -R 777 /tmp/ms-playwright + +ENV PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright +``` + +### ⚠️ AÇÃO NECESSÁRIA: Adicionar em K8s Manifests + +**Adicionar em `k8s-manifests-complete.yaml` - archon-server deployment:** + +```yaml +spec: + template: + spec: + containers: + - name: server + env: + # ... outras variáveis ... + + # ADICIONAR ESTA LINHA: + - name: PLAYWRIGHT_BROWSERS_PATH + value: "/app/ms-playwright" +``` + +--- + +## 2. Resource Limits para Crawling com Chromium + +### Problema +Chromium consome significativa memória e CPU durante crawling. Os limites atuais podem ser insuficientes: + +**Atual:** +```yaml +resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" +``` + +### Solução Recomendada + +**Atualizar em `k8s-manifests-complete.yaml` - archon-server:** + +```yaml +resources: + requests: + memory: "768Mi" # Aumentado de 512Mi + cpu: "500m" + limits: + memory: "2Gi" # Aumentado de 1Gi (Chromium pode usar 1.5Gi em picos) + cpu: "2000m" # Aumentado de 1000m (crawling paralelo) + + # ADICIONAR: Limitar uso de ephemeral storage + ephemeral-storage: "5Gi" +``` + +### Justificativa +- Chromium headless consome ~300-600MB por instância +- Crawling paralelo pode executar múltiplas instâncias +- Processamento de documentos grandes precisa de memória +- Margem de segurança para evitar OOMKilled + +--- + +## 3. Nginx Configuration e Permissions + +### Status Atual +✅ Nginx já configurado para rodar como non-root (user `nginx`, UID 101) + +**Dockerfile.k8s.production:** +```dockerfile +RUN chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx /var/log/nginx /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +USER nginx +``` + +### ⚠️ Melhorias Recomendadas + +**Adicionar em `k8s-manifests-complete.yaml` - archon-frontend:** + +```yaml +spec: + template: + spec: + securityContext: + runAsNonRoot: true + runAsUser: 101 # nginx user + runAsGroup: 101 + fsGroup: 101 + # ADICIONAR: + seccompProfile: + type: RuntimeDefault + + containers: + - name: frontend + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + # Nginx não precisa de capabilities especiais na porta 3737 + readOnlyRootFilesystem: true # MUDAR para true + + # ADICIONAR volumes para diretórios que nginx precisa escrever: + volumeMounts: + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-run + mountPath: /var/run + - name: nginx-logs + mountPath: /var/log/nginx + + volumes: + - name: nginx-cache + emptyDir: {} + - name: nginx-run + emptyDir: {} + - name: nginx-logs + emptyDir: {} +``` + +--- + +## 4. Advanced Security Contexts + +### Problema +Security contexts estão básicos. Podem ser fortalecidos para melhor segurança. + +### Solução: Pod Security Standards + +**Adicionar em TODOS os deployments:** + +```yaml +spec: + template: + metadata: + labels: + app: archon-server # ou mcp, frontend, etc + # ADICIONAR: + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/warn: restricted + + spec: + # Security context do pod + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + # ADICIONAR: + seccompProfile: + type: RuntimeDefault + supplementalGroups: [] + + # Security context do container + containers: + - name: server + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1001 + capabilities: + drop: + - ALL + # ADICIONAR (se possível - testar primeiro): + readOnlyRootFilesystem: false # true após configurar volumes + seccompProfile: + type: RuntimeDefault +``` + +### Arquivos que Precisam Escrever + +**archon-server:** +- `/app/ms-playwright` - Playwright browser cache (já configurado com ownership correto) +- `/tmp` - Temporary files (já acessível para appuser) +- Nenhum volume persistente necessário (tudo vai para Supabase) + +**archon-mcp e archon-agents:** +- Nenhum arquivo local necessário +- Podem usar `readOnlyRootFilesystem: true` + +--- + +## 5. Health Checks Otimizados + +### Problema Atual +Health checks podem ser muito agressivos durante operações pesadas (crawling). + +### Solução + +**Atualizar em `k8s-manifests-complete.yaml` - archon-server:** + +```yaml +livenessProbe: + httpGet: + path: /health + port: 8181 + initialDelaySeconds: 60 # Aumentado de 40 (tempo para Playwright inicializar) + periodSeconds: 30 # OK + timeoutSeconds: 15 # Aumentado de 10 (crawling pode deixar servidor lento) + failureThreshold: 5 # Aumentado de 3 (mais tolerante) + successThreshold: 1 + +readinessProbe: + httpGet: + path: /health + port: 8181 + initialDelaySeconds: 15 # Aumentado de 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + +# ADICIONAR startup probe para não matar pod durante startup lento: +startupProbe: + httpGet: + path: /health + port: 8181 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 12 # 12 x 10s = 2 minutos para startup + successThreshold: 1 +``` + +--- + +## 6. Init Container para Playwright Warm-up (Opcional mas Recomendado) + +### Problema +Primeira requisição de crawling é lenta porque Playwright precisa inicializar. + +### Solução + +**Adicionar em `k8s-manifests-complete.yaml` - archon-server:** + +```yaml +spec: + template: + spec: + # ADICIONAR antes de containers: + initContainers: + - name: playwright-warmup + image: git.automatizase.com.br/luis.erlacher/archon/server:k8s-latest + imagePullPolicy: Always + command: + - sh + - -c + - | + echo "Verificando instalação do Playwright..." + python -c "from playwright.sync_api import sync_playwright; print('Playwright OK')" || exit 1 + echo "Playwright inicializado com sucesso" + env: + - name: PLAYWRIGHT_BROWSERS_PATH + value: "/app/ms-playwright" + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "512Mi" + cpu: "500m" + securityContext: + runAsNonRoot: true + runAsUser: 1001 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + + containers: + - name: server + # ... resto da configuração ... +``` + +--- + +## 7. ConfigMap Updates + +### Adicionar Playwright e outras configurações + +**Atualizar em `k8s-manifests-complete.yaml` - ConfigMap:** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: archon-config + namespace: archon +data: + # Existing configs... + SERVICE_DISCOVERY_MODE: "kubernetes" + LOG_LEVEL: "INFO" + ARCHON_SERVER_PORT: "8181" + ARCHON_MCP_PORT: "8051" + ARCHON_UI_PORT: "3737" + ARCHON_HOST: "localhost" + TRANSPORT: "sse" + AGENTS_ENABLED: "false" + + # ADICIONAR: + PLAYWRIGHT_BROWSERS_PATH: "/app/ms-playwright" + + # MCP Public URL - IMPORTANTE: Configure com seu domínio! + # Format: "domain.com:8051" or "localhost:8051" + # Examples: + # - Development: localhost:8051 + # - Production: archon.automatizase.com.br:8051 + # - Custom: mcp.mycompany.com:8051 + # This is used to generate MCP client configuration JSON + MCP_PUBLIC_URL: "archon.automatizase.com.br:8051" # ← CHANGE THIS! + + # Chromium optimization flags (já configurados no código, mas podem ser sobrescritos): + CHROMIUM_DISABLE_DEV_SHM: "true" + CHROMIUM_HEADLESS: "true" +``` + +--- + +## 8. Network Policies (Segurança Adicional) + +### Criar Network Policy para isolar pods + +**Criar arquivo `k8s-network-policies.yaml`:** + +```yaml +--- +# Network Policy - Archon Server +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: archon-server-netpol + namespace: archon +spec: + podSelector: + matchLabels: + app: archon-server + policyTypes: + - Ingress + - Egress + ingress: + # Permite tráfego do frontend + - from: + - podSelector: + matchLabels: + app: archon-frontend + ports: + - protocol: TCP + port: 8181 + # Permite tráfego do MCP + - from: + - podSelector: + matchLabels: + app: archon-mcp + ports: + - protocol: TCP + port: 8181 + egress: + # Permite DNS + - to: + - namespaceSelector: + matchLabels: + name: kube-system + ports: + - protocol: UDP + port: 53 + # Permite Supabase (internet) + - to: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 443 + # Permite comunicação com MCP + - to: + - podSelector: + matchLabels: + app: archon-mcp + ports: + - protocol: TCP + port: 8051 + +--- +# Network Policy - Archon MCP +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: archon-mcp-netpol + namespace: archon +spec: + podSelector: + matchLabels: + app: archon-mcp + policyTypes: + - Ingress + - Egress + ingress: + # Permite tráfego do server + - from: + - podSelector: + matchLabels: + app: archon-server + ports: + - protocol: TCP + port: 8051 + egress: + # Permite DNS + - to: + - namespaceSelector: + matchLabels: + name: kube-system + ports: + - protocol: UDP + port: 53 + # Permite Supabase + - to: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 443 + # Permite comunicação com server + - to: + - podSelector: + matchLabels: + app: archon-server + ports: + - protocol: TCP + port: 8181 + +--- +# Network Policy - Archon Frontend +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: archon-frontend-netpol + namespace: archon +spec: + podSelector: + matchLabels: + app: archon-frontend + policyTypes: + - Ingress + - Egress + ingress: + # Permite tráfego de qualquer lugar (public-facing) + - {} + egress: + # Permite DNS + - to: + - namespaceSelector: + matchLabels: + name: kube-system + ports: + - protocol: UDP + port: 53 + # Permite comunicação com server (para API calls) + - to: + - podSelector: + matchLabels: + app: archon-server + ports: + - protocol: TCP + port: 8181 +``` + +--- + +## 9. Horizontal Pod Autoscaling (HPA) + +### Configurar autoscaling para server + +**Criar arquivo `k8s-hpa.yaml`:** + +```yaml +--- +# HPA - Archon Server (crawling pode ter spikes de carga) +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: archon-server-hpa + namespace: archon +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: archon-server + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 # Espera 5min antes de scale down + policies: + - type: Percent + value: 50 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 30 # Scale up rápido + policies: + - type: Percent + value: 100 + periodSeconds: 30 + +--- +# HPA - Frontend (menos crítico, pode ser fixo em 2 réplicas) +# Opcional se houver muito tráfego +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: archon-frontend-hpa + namespace: archon +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: archon-frontend + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 +``` + +--- + +## 10. PodDisruptionBudget (Alta Disponibilidade) + +### Garantir disponibilidade durante rolling updates + +**Criar arquivo `k8s-pdb.yaml`:** + +```yaml +--- +# PDB - Archon Server +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: archon-server-pdb + namespace: archon +spec: + minAvailable: 1 + selector: + matchLabels: + app: archon-server + unhealthyPodEvictionPolicy: AlwaysAllow + +--- +# PDB - Archon Frontend +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: archon-frontend-pdb + namespace: archon +spec: + minAvailable: 1 + selector: + matchLabels: + app: archon-frontend + unhealthyPodEvictionPolicy: AlwaysAllow +``` + +--- + +## 11. Persistent Volumes - NÃO NECESSÁRIO + +### Análise de Necessidade + +**✅ Arquon NÃO precisa de volumes persistentes porque:** + +1. **Uploads de documentos**: Processados em memória e salvos no Supabase +2. **Crawling results**: Salvos diretamente no Supabase +3. **Playwright cache**: Reinstalado na inicialização do pod (stateless) +4. **Logs**: Enviados para stdout/stderr (capturados pelo K8s) +5. **Credenciais**: Armazenadas no Supabase (encrypted) +6. **Session data**: Gerenciado por Socket.IO em memória + +**📊 Arquitetura Stateless:** +``` +Pod → Processa dados → Salva no Supabase → Pod morre → Novo pod funciona igual +``` + +**⚠️ Exceção:** Se precisar de cache local para performance: +```yaml +# Opcional: Volume efêmero para cache de embeddings (não persiste entre restarts) +volumes: +- name: embedding-cache + emptyDir: + sizeLimit: 1Gi +``` + +--- + +## 12. Monitoring e Observability + +### Prometheus Metrics (Recomendado) + +**Adicionar annotations nos deployments:** + +```yaml +spec: + template: + metadata: + annotations: + # ADICIONAR: + prometheus.io/scrape: "true" + prometheus.io/port: "8181" # ou 8051 para MCP + prometheus.io/path: "/metrics" # Se implementar endpoint +``` + +### Logfire Integration + +**Verificar em `k8s-manifests-complete.yaml` - Secrets:** + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: archon-secrets + namespace: archon +type: Opaque +stringData: + SUPABASE_URL: "https://seu-projeto.supabase.co" + SUPABASE_SERVICE_KEY: "sua-service-role-key-aqui" + OPENAI_API_KEY: "sua-openai-key-aqui" + LOGFIRE_TOKEN: "seu-logfire-token-aqui" # CONFIGURAR se usar Logfire +``` + +--- + +## Checklist de Implementação + +### 🔴 PRIORIDADE CRÍTICA (Impede funcionamento) +- [x] ✅ Corrigir Playwright browser path nos Dockerfiles +- [x] ✅ Adicionar `PLAYWRIGHT_BROWSERS_PATH` env var no deployment K8s +- [x] ✅ Adicionar `MCP_PUBLIC_URL` no ConfigMap e deployment K8s +- [x] ✅ Aumentar resource limits (memory: 2Gi, cpu: 2000m) +- [ ] ⚠️ Configurar `MCP_PUBLIC_URL` com o domínio correto no ConfigMap +- [ ] ⚠️ Rebuild e push das imagens K8s + +### 🟡 PRIORIDADE ALTA (Segurança e estabilidade) +- [x] ✅ Atualizar health checks (startup probe, failureThreshold) +- [ ] ⚠️ Adicionar security contexts avançados (seccompProfile, readOnlyRootFilesystem) +- [ ] ⚠️ Configurar volumes para nginx (cache, run, logs) +- [ ] ⚠️ Implementar Network Policies + +### 🟢 PRIORIDADE MÉDIA (Performance e observabilidade) +- [ ] 🔄 Adicionar init container para Playwright warm-up +- [ ] 🔄 Configurar HPA para server +- [ ] 🔄 Configurar PodDisruptionBudget +- [ ] 🔄 Adicionar Prometheus annotations + +### 🔵 PRIORIDADE BAIXA (Melhoria contínua) +- [ ] 📝 Implementar /metrics endpoint para Prometheus +- [ ] 📝 Configurar Logfire token +- [ ] 📝 Testar readOnlyRootFilesystem: true no server +- [ ] 📝 Considerar resource quotas por namespace + +--- + +## Comandos para Deploy + +### 1. Rebuild e Push das Imagens + +```bash +# Server +cd /home/lperl/Archon +docker build -f python/Dockerfile.k8s.server -t git.automatizase.com.br/luis.erlacher/archon/server:k8s-latest python/ +docker push git.automatizase.com.br/luis.erlacher/archon/server:k8s-latest + +# MCP (não mudou, mas rebuild para garantir) +docker build -f python/Dockerfile.k8s.mcp -t git.automatizase.com.br/luis.erlacher/archon/mcp:k8s-latest python/ +docker push git.automatizase.com.br/luis.erlacher/archon/mcp:k8s-latest + +# Frontend (não mudou, mas rebuild para garantir) +docker build -f archon-ui-main/Dockerfile.k8s.production -t git.automatizase.com.br/luis.erlacher/archon/frontend:k8s-latest archon-ui-main/ +docker push git.automatizase.com.br/luis.erlacher/archon/frontend:k8s-latest + +# Agents (se usado) +docker build -f python/Dockerfile.k8s.agents -t git.automatizase.com.br/luis.erlacher/archon/agents:k8s-latest python/ +docker push git.automatizase.com.br/luis.erlacher/archon/agents:k8s-latest +``` + +### 2. Aplicar K8s Manifests + +```bash +# Namespace e secrets (se ainda não existir) +kubectl apply -f k8s-manifests-complete.yaml + +# Network policies (criar arquivo primeiro) +kubectl apply -f k8s-network-policies.yaml + +# HPA (criar arquivo primeiro) +kubectl apply -f k8s-hpa.yaml + +# PDB (criar arquivo primeiro) +kubectl apply -f k8s-pdb.yaml +``` + +### 3. Rolling Restart + +```bash +# Restart server (vai pegar nova imagem) +kubectl rollout restart deployment/archon-server -n archon +kubectl rollout status deployment/archon-server -n archon + +# Restart MCP +kubectl rollout restart deployment/archon-mcp -n archon +kubectl rollout status deployment/archon-mcp -n archon + +# Restart frontend +kubectl rollout restart deployment/archon-frontend -n archon +kubectl rollout status deployment/archon-frontend -n archon +``` + +### 4. Verificar Status + +```bash +# Ver pods +kubectl get pods -n archon -w + +# Ver logs do server +kubectl logs -f deployment/archon-server -n archon + +# Ver eventos +kubectl get events -n archon --sort-by='.lastTimestamp' + +# Testar crawling +kubectl port-forward -n archon svc/archon-server-service 8181:8181 +# Em outro terminal: +curl -X POST http://localhost:8181/api/knowledge/crawl \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com"}' +``` + +--- + +## Troubleshooting + +### Problema: Pod crashando com OOMKilled +**Solução:** Aumentar memory limits para 2Gi ou mais + +### Problema: Playwright ainda não encontra browser +**Verificar:** +```bash +kubectl exec -it deployment/archon-server -n archon -- bash +echo $PLAYWRIGHT_BROWSERS_PATH +ls -la /app/ms-playwright +``` + +### Problema: Health check falhando +**Solução:** Aumentar `initialDelaySeconds` e `failureThreshold` + +### Problema: Rolling update com downtime +**Solução:** Verificar PodDisruptionBudget e garantir minAvailable: 1 + +--- + +## Referências + +- [Kubernetes Best Practices](https://kubernetes.io/docs/concepts/configuration/overview/) +- [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) +- [Playwright in Docker](https://playwright.dev/docs/docker) +- [Nginx Non-Root](https://hub.docker.com/_/nginx) +- [FastAPI Deployment](https://fastapi.tiangolo.com/deployment/docker/) diff --git a/MCP_PUBLIC_URL_GUIDE.md b/MCP_PUBLIC_URL_GUIDE.md new file mode 100644 index 0000000..d6367aa --- /dev/null +++ b/MCP_PUBLIC_URL_GUIDE.md @@ -0,0 +1,452 @@ +# MCP Public URL Configuration Guide + +## Overview + +The `MCP_PUBLIC_URL` environment variable allows you to configure the publicly accessible URL for the MCP server. This is used to generate the correct client configuration JSON for MCP-compatible IDEs (Claude Code, Cursor, Windsurf, etc.). + +## Why This Feature Exists + +### The Problem + +When Archon is deployed on Kubernetes or behind a reverse proxy, the MCP server needs to provide client configuration with the **publicly accessible domain**, not `localhost`. + +**Before this feature:** +```json +{ + "mcpServers": { + "archon": { + "url": "http://localhost:8051/mcp" ← ❌ Doesn't work from external machines! + } + } +} +``` + +**After this feature:** +```json +{ + "mcpServers": { + "archon": { + "url": "http://archon.automatizase.com.br:8051/mcp" ← ✅ Works from anywhere! + } + } +} +``` + +## Configuration + +### Format + +The `MCP_PUBLIC_URL` variable accepts the following formats: + +```bash +# With explicit port +MCP_PUBLIC_URL="archon.automatizase.com.br:8051" + +# Domain only (port will be inferred from ARCHON_MCP_PORT) +MCP_PUBLIC_URL="archon.automatizase.com.br" + +# Development (default) +MCP_PUBLIC_URL="localhost:8051" +``` + +### Docker Compose + +Edit your `.env` file: + +```bash +# MCP Public URL Configuration +# Used to generate client configuration JSON for Claude Code, Cursor, etc. +MCP_PUBLIC_URL=localhost:8051 # Change to your domain for production +``` + +Or export in your shell: + +```bash +export MCP_PUBLIC_URL="archon.yourdomain.com:8051" +docker compose up -d +``` + +### Kubernetes + +Edit `k8s-manifests-complete.yaml` - **ConfigMap section:** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: archon-config + namespace: archon +data: + # ... other configs ... + + # MCP Public URL - CHANGE THIS TO YOUR DOMAIN! + MCP_PUBLIC_URL: "archon.automatizase.com.br:8051" # ← UPDATE THIS +``` + +Then apply: + +```bash +kubectl apply -f k8s-manifests-complete.yaml +kubectl rollout restart deployment/archon-server -n archon +``` + +## How It Works + +### Backend (Python) + +**File:** `python/src/server/api_routes/mcp_api.py` + +The `/api/mcp/config` endpoint reads `MCP_PUBLIC_URL` and uses it to generate the configuration: + +```python +# Get MCP public URL from environment +mcp_public_url = os.getenv("MCP_PUBLIC_URL") + +if mcp_public_url: + # Parse to extract host and port + if ":" in mcp_public_url: + host, port_str = mcp_public_url.rsplit(":", 1) + port = int(port_str) + else: + host = mcp_public_url + port = int(os.getenv("ARCHON_MCP_PORT", "8051")) +else: + # Fallback to localhost + host = "localhost" + port = 8051 + +config = { + "host": host, + "port": port, + "transport": "streamable-http" +} +``` + +### Frontend (React) + +**File:** `archon-ui-main/src/features/mcp/components/McpConfigSection.tsx` + +The frontend fetches the config and generates IDE-specific JSON: + +```typescript +// For Claude Code +{ + "name": "archon", + "transport": "http", + "url": `http://${config.host}:${config.port}/mcp` +} + +// For Cursor +{ + "mcpServers": { + "archon": { + "url": `http://${config.host}:${config.port}/mcp` + } + } +} + +// For Windsurf +{ + "mcpServers": { + "archon": { + "serverUrl": `http://${config.host}:${config.port}/mcp` + } + } +} + +// And so on for all supported IDEs... +``` + +## Deployment Scenarios + +### Local Development + +```bash +# .env +MCP_PUBLIC_URL=localhost:8051 +``` + +Generated URL: `http://localhost:8051/mcp` + +### Production - Direct Access + +If MCP is directly accessible on the public internet: + +```bash +# Kubernetes ConfigMap +MCP_PUBLIC_URL: "archon.mycompany.com:8051" +``` + +Generated URL: `http://archon.mycompany.com:8051/mcp` + +### Production - Behind Reverse Proxy + +If MCP is behind Nginx/Traefik on standard HTTP port: + +```bash +# Kubernetes ConfigMap +MCP_PUBLIC_URL: "mcp.mycompany.com:80" +# Or if reverse proxy handles port mapping: +MCP_PUBLIC_URL: "mcp.mycompany.com" # Port inferred from ARCHON_MCP_PORT +``` + +Generated URL: `http://mcp.mycompany.com/mcp` + +### Production - HTTPS with Custom Port + +```bash +# Note: Frontend still generates http:// URLs +# Your reverse proxy should handle HTTPS termination +MCP_PUBLIC_URL: "archon.mycompany.com:443" +``` + +**Important:** The MCP protocol uses HTTP URLs even when behind HTTPS. Your reverse proxy or load balancer should handle SSL termination. + +## Verification + +### 1. Check Backend Config Endpoint + +```bash +curl http://localhost:8181/api/mcp/config | jq +``` + +Expected output: + +```json +{ + "host": "archon.automatizase.com.br", + "port": 8051, + "transport": "streamable-http", + "model_choice": "gpt-4o-mini" +} +``` + +### 2. Check Frontend MCP Page + +1. Open Archon UI: `http://localhost:3737` +2. Navigate to MCP page +3. Select an IDE (e.g., Claude Code) +4. Verify the generated command/JSON contains your domain: + +```bash +# Should show: +claude mcp add --transport http archon http://archon.automatizase.com.br:8051/mcp +``` + +### 3. Test from External Machine + +From another machine, try the MCP connection: + +```bash +curl http://archon.automatizase.com.br:8051/health +``` + +Should return: + +```json +{ + "status": "ok", + "version": "..." +} +``` + +## Troubleshooting + +### Problem: Still showing localhost + +**Check:** +1. Is `MCP_PUBLIC_URL` set in ConfigMap? + ```bash + kubectl get configmap archon-config -n archon -o yaml | grep MCP_PUBLIC_URL + ``` + +2. Did you restart the server deployment? + ```bash + kubectl rollout restart deployment/archon-server -n archon + ``` + +3. Check server logs: + ```bash + kubectl logs -f deployment/archon-server -n archon | grep MCP_PUBLIC_URL + ``` + + Should see: + ``` + Using MCP_PUBLIC_URL - host=archon.automatizase.com.br, port=8051 + ``` + +### Problem: Port not included in URL + +**Solution:** Explicitly include the port in `MCP_PUBLIC_URL`: + +```bash +# Instead of: +MCP_PUBLIC_URL="archon.mycompany.com" + +# Use: +MCP_PUBLIC_URL="archon.mycompany.com:8051" +``` + +### Problem: Can't connect from IDE + +**Check:** +1. **Firewall:** Is port 8051 open? + ```bash + telnet archon.automatizase.com.br 8051 + ``` + +2. **MCP Service:** Is it running? + ```bash + kubectl get pods -n archon | grep mcp + ``` + +3. **Network Policy:** Do you have network policies blocking ingress? + ```bash + kubectl get networkpolicies -n archon + ``` + +## Security Considerations + +### 1. MCP Exposes Read/Write Access + +MCP tools can: +- Search and read your knowledge base +- Create/update/delete projects and tasks +- Execute searches +- Modify data in Supabase + +**Recommendation:** +- Use authentication (future feature) +- Restrict access via firewall/network policies +- Don't expose MCP publicly without authentication + +### 2. No Built-in Authentication (Yet) + +Currently, anyone who can reach `http://your-domain:8051/mcp` can use your MCP server. + +**Mitigation strategies:** +- Use Kubernetes Network Policies to restrict access +- Use VPN or private networking +- Put MCP behind a reverse proxy with authentication +- Use IP allowlisting on your firewall + +### 3. HTTPS Considerations + +MCP protocol uses `http://` URLs even when behind HTTPS. This is normal - your reverse proxy handles SSL termination. + +**Example setup:** +``` +User → HTTPS (443) → Reverse Proxy → HTTP (8051) → MCP Pod +``` + +## Advanced Configuration + +### Multiple Environments + +Use different ConfigMaps per environment: + +```bash +# dev-config.yaml +MCP_PUBLIC_URL: "localhost:8051" + +# staging-config.yaml +MCP_PUBLIC_URL: "staging.archon.mycompany.com:8051" + +# prod-config.yaml +MCP_PUBLIC_URL: "archon.mycompany.com:8051" +``` + +### Custom Domain with Ingress + +If using Kubernetes Ingress: + +```yaml +# ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: archon-mcp-ingress + namespace: archon +spec: + rules: + - host: mcp.mycompany.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: archon-mcp-service + port: + number: 8051 +``` + +Then set: +```yaml +# ConfigMap +MCP_PUBLIC_URL: "mcp.mycompany.com:80" # or just "mcp.mycompany.com" +``` + +## Related Configuration + +### ARCHON_HOST (Legacy) + +The old `ARCHON_HOST` variable is still used as a fallback if `MCP_PUBLIC_URL` is not set: + +```bash +# Legacy mode (still works) +ARCHON_HOST=localhost +ARCHON_MCP_PORT=8051 + +# New mode (preferred) +MCP_PUBLIC_URL=localhost:8051 +``` + +**Migration path:** +1. Add `MCP_PUBLIC_URL` with your production domain +2. Keep `ARCHON_HOST` for backwards compatibility +3. Eventually, `ARCHON_HOST` may be removed + +### MCP_SERVICE_URL (Internal) + +**Do not confuse** `MCP_PUBLIC_URL` with `MCP_SERVICE_URL`: + +- `MCP_PUBLIC_URL`: **External** URL for client configuration (e.g., `archon.mycompany.com:8051`) +- `MCP_SERVICE_URL`: **Internal** K8s DNS for server-to-MCP communication (e.g., `http://archon-mcp-service.archon.svc.cluster.local:8051`) + +## Summary + +**Quick Setup:** + +1. **Edit ConfigMap:** + ```yaml + MCP_PUBLIC_URL: "your-domain.com:8051" + ``` + +2. **Apply and restart:** + ```bash + kubectl apply -f k8s-manifests-complete.yaml + kubectl rollout restart deployment/archon-server -n archon + ``` + +3. **Verify:** + ```bash + curl http://localhost:8181/api/mcp/config | jq .host + # Should return: "your-domain.com" + ``` + +4. **Test from IDE:** + - Open Archon UI → MCP page + - Copy configuration for your IDE + - Verify URL contains your domain + +**Files Modified:** +- ✅ `python/src/server/api_routes/mcp_api.py` - Backend logic +- ✅ `k8s-manifests-complete.yaml` - K8s ConfigMap and deployment +- ✅ `docker-compose.yml` - Docker Compose environment +- ✅ `.env.example` - Environment variable documentation +- ✅ `K8S_COMPLETE_ADJUSTMENTS.md` - Deployment guide + +**Support:** +- GitHub Issues: https://github.com/your-repo/archon/issues +- Documentation: https://docs.archon.yourdomain.com diff --git a/docker-compose.yml b/docker-compose.yml index 9d1e588..84ac15c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,8 @@ services: - ARCHON_AGENTS_PORT=${ARCHON_AGENTS_PORT:-8052} - AGENTS_ENABLED=${AGENTS_ENABLED:-false} - ARCHON_HOST=${HOST:-localhost} + - PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright + - MCP_PUBLIC_URL=${MCP_PUBLIC_URL:-localhost:8051} networks: - app-network volumes: diff --git a/k8s-manifests-complete.yaml b/k8s-manifests-complete.yaml index ea53a25..0e1ee0e 100644 --- a/k8s-manifests-complete.yaml +++ b/k8s-manifests-complete.yaml @@ -49,6 +49,17 @@ data: TRANSPORT: "sse" AGENTS_ENABLED: "false" + # Playwright Configuration + PLAYWRIGHT_BROWSERS_PATH: "/app/ms-playwright" + + # MCP Public URL Configuration + # Format: "domain.com" or "domain.com:8051" or "localhost:8051" + # This is used to generate the MCP client configuration JSON + # IMPORTANTE: Configure this with your actual domain! + # Example: "archon.automatizase.com.br" (port will be inferred from ARCHON_MCP_PORT) + # Example: "archon.automatizase.com.br:8051" (explicit port) + MCP_PUBLIC_URL: "localhost:8051" # CHANGE THIS TO YOUR DOMAIN! + --- # ============================================================================= # DEPLOYMENT - ARCHON SERVER (Backend Principal) @@ -151,28 +162,49 @@ spec: - name: MCP_SERVICE_URL value: "http://archon-mcp-service.archon.svc.cluster.local:8051" + # Playwright configuration + - name: PLAYWRIGHT_BROWSERS_PATH + value: "/app/ms-playwright" + + # MCP Public URL (for client configuration) + - name: MCP_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: archon-config + key: MCP_PUBLIC_URL + resources: requests: - memory: "512Mi" + memory: "768Mi" cpu: "500m" limits: - memory: "1Gi" - cpu: "1000m" + memory: "2Gi" + cpu: "2000m" + ephemeral-storage: "5Gi" + + startupProbe: + httpGet: + path: /health + port: 8181 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 12 livenessProbe: httpGet: path: /health port: 8181 - initialDelaySeconds: 40 + initialDelaySeconds: 60 periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 + timeoutSeconds: 15 + failureThreshold: 5 readinessProbe: httpGet: path: /health port: 8181 - initialDelaySeconds: 10 + initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 diff --git a/python/Dockerfile.k8s.server b/python/Dockerfile.k8s.server index 8aafec0..5c25f4a 100644 --- a/python/Dockerfile.k8s.server +++ b/python/Dockerfile.k8s.server @@ -64,10 +64,12 @@ RUN groupadd -r appuser -g 1001 && \ # Copy the virtual environment from builder COPY --from=builder --chown=appuser:appuser /venv /venv -# Install Playwright browsers as root (needs permissions) +# Install Playwright browsers in a location accessible to appuser ENV PATH=/venv/bin:$PATH -RUN playwright install chromium && \ - chown -R appuser:appuser /root/.cache/ms-playwright 2>/dev/null || true +ENV PLAYWRIGHT_BROWSERS_PATH=/app/ms-playwright +RUN mkdir -p /app/ms-playwright && \ + playwright install chromium && \ + chown -R appuser:appuser /app/ms-playwright # Copy server code (production only - no tests) COPY --chown=appuser:appuser src/server/ src/server/ @@ -77,6 +79,7 @@ COPY --chown=appuser:appuser src/__init__.py src/ ENV PYTHONPATH="/app:$PYTHONPATH" ENV PYTHONUNBUFFERED=1 ENV PATH="/venv/bin:$PATH" +ENV PLAYWRIGHT_BROWSERS_PATH=/app/ms-playwright # Expose Server port ARG ARCHON_SERVER_PORT=8181 diff --git a/python/Dockerfile.server b/python/Dockerfile.server index 6e0ea55..9bb525b 100644 --- a/python/Dockerfile.server +++ b/python/Dockerfile.server @@ -51,9 +51,12 @@ RUN apt-get update && apt-get install -y \ # Copy the virtual environment from builder COPY --from=builder /venv /venv -# Install Playwright browsers +# Install Playwright browsers in accessible location ENV PATH=/venv/bin:$PATH -RUN playwright install chromium +ENV PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright +RUN mkdir -p /tmp/ms-playwright && \ + playwright install chromium && \ + chmod -R 777 /tmp/ms-playwright # Copy server code and tests COPY src/server/ src/server/ @@ -64,6 +67,7 @@ COPY tests/ tests/ ENV PYTHONPATH="/app:$PYTHONPATH" ENV PYTHONUNBUFFERED=1 ENV PATH="/venv/bin:$PATH" +ENV PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright # Expose Server port ARG ARCHON_SERVER_PORT=8181 diff --git a/python/src/mcp_server/mcp_server.py b/python/src/mcp_server/mcp_server.py index eac6040..02416d7 100644 --- a/python/src/mcp_server/mcp_server.py +++ b/python/src/mcp_server/mcp_server.py @@ -31,6 +31,8 @@ from typing import Any from dotenv import load_dotenv from mcp.server.fastmcp import Context, FastMCP +from starlette.requests import Request +from starlette.responses import JSONResponse # Add the project root to Python path for imports sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) @@ -332,7 +334,31 @@ except Exception as e: raise -# Health check endpoint +# HTTP health check endpoint for K8s and frontend polling +@mcp.custom_route("/health", methods=["GET"]) +async def http_health_check(request: Request) -> JSONResponse: + """ + HTTP health check endpoint for K8s probes and frontend monitoring. + + This is a lightweight endpoint that returns 200 OK to indicate the server is running. + For detailed health checks including dependent services, use the health_check tool. + """ + try: + # Quick health check without heavy dependencies + return JSONResponse({ + "status": "healthy", + "service": "mcp", + "timestamp": datetime.now().isoformat(), + }) + except Exception as e: + logger.error(f"HTTP health check failed: {e}") + return JSONResponse( + {"status": "unhealthy", "error": str(e)}, + status_code=503 + ) + + +# MCP tool health check endpoint (detailed health status) @mcp.tool() async def health_check(ctx: Context) -> str: """ diff --git a/python/src/server/api_routes/mcp_api.py b/python/src/server/api_routes/mcp_api.py index 7ea7b47..7e34506 100644 --- a/python/src/server/api_routes/mcp_api.py +++ b/python/src/server/api_routes/mcp_api.py @@ -172,13 +172,36 @@ async def get_mcp_config(): try: api_logger.info("Getting MCP server configuration") - # Get actual MCP port from environment or use default - mcp_port = int(os.getenv("ARCHON_MCP_PORT", "8051")) + # Get MCP public URL from environment (for production/Kubernetes) + # Format: "domain.com:8051" or "localhost:8051" + mcp_public_url = os.getenv("MCP_PUBLIC_URL") - # Configuration for streamable-http mode with actual port + if mcp_public_url: + # Parse public URL to extract host and port + if ":" in mcp_public_url: + host, port_str = mcp_public_url.rsplit(":", 1) + try: + port = int(port_str) + except ValueError: + # If port is not a number, use default + host = mcp_public_url + port = int(os.getenv("ARCHON_MCP_PORT", "8051")) + else: + # No port in URL, use default + host = mcp_public_url + port = int(os.getenv("ARCHON_MCP_PORT", "8051")) + + api_logger.info(f"Using MCP_PUBLIC_URL - host={host}, port={port}") + else: + # Fallback to legacy behavior (localhost) + host = os.getenv("ARCHON_HOST", "localhost") + port = int(os.getenv("ARCHON_MCP_PORT", "8051")) + api_logger.info(f"Using legacy ARCHON_HOST - host={host}, port={port}") + + # Configuration for streamable-http mode config = { - "host": os.getenv("ARCHON_HOST", "localhost"), - "port": mcp_port, + "host": host, + "port": port, "transport": "streamable-http", } From 65348bfed0c74d82c720b402317f88a9a37b9567 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 04:13:17 +0000 Subject: [PATCH 2/3] feat: Add Supabase authentication with login/signup pages Implements complete authentication system using Supabase Auth SDK following Archon's vertical slice architecture. Frontend Changes: - Install @supabase/supabase-js dependency - Create auth feature in vertical slice pattern: * AuthContext and Provider for global auth state * authService with Supabase Auth methods (signIn, signUp, signOut, etc.) * Auth query hooks with TanStack Query integration * TypeScript types for User, Session, AuthState * ProtectedRoute component for route guards - Add LoginPage and SignUpPage with Tron-themed design - Update App.tsx with AuthProvider and protected routes - Configure Supabase client with environment variables Backend Changes: - Create auth_service.py for JWT token validation - Create auth_middleware.py for protecting API routes (optional, commented by default) - Create auth_api.py with endpoints: * POST /api/auth/verify - Verify JWT token * GET /api/auth/user - Get current user * GET /api/auth/health - Auth service health check - Register auth router in main.py - Add middleware configuration (disabled by default) Configuration: - Update .env.example with VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY - Add comprehensive AUTHENTICATION_SETUP.md documentation Features: - Email/password authentication - Persistent sessions with localStorage - Auto token refresh - Route protection with loading states - Integration with existing TanStack Query patterns - Optional backend middleware for API protection - Row Level Security (RLS) ready Architecture follows CLAUDE.md guidelines: - Vertical slice architecture for auth feature - TanStack Query for state management - No backwards compatibility needed (beta) - KISS principle - Fail fast with detailed errors Notes: - Auth middleware is commented out by default to avoid breaking existing installations - Users can enable it when ready by uncommenting in main.py - Frontend auth works independently of backend middleware - Comprehensive setup guide included in AUTHENTICATION_SETUP.md --- .env.example | 7 + AUTHENTICATION_SETUP.md | 283 ++++++++++++++++++ archon-ui-main/package-lock.json | 98 +++++- archon-ui-main/package.json | 1 + archon-ui-main/src/App.tsx | 40 ++- .../auth/components/ProtectedRoute.tsx | 28 ++ .../features/auth/config/supabaseClient.ts | 19 ++ .../src/features/auth/context/AuthContext.tsx | 105 +++++++ .../src/features/auth/hooks/useAuthQueries.ts | 67 +++++ .../src/features/auth/services/authService.ts | 111 +++++++ .../src/features/auth/types/index.ts | 30 ++ archon-ui-main/src/pages/LoginPage.tsx | 93 ++++++ archon-ui-main/src/pages/SignUpPage.tsx | 148 +++++++++ python/src/server/api_routes/auth_api.py | 75 +++++ python/src/server/main.py | 7 + .../src/server/middleware/auth_middleware.py | 48 +++ python/src/server/services/auth_service.py | 97 ++++++ 17 files changed, 1238 insertions(+), 19 deletions(-) create mode 100644 AUTHENTICATION_SETUP.md create mode 100644 archon-ui-main/src/features/auth/components/ProtectedRoute.tsx create mode 100644 archon-ui-main/src/features/auth/config/supabaseClient.ts create mode 100644 archon-ui-main/src/features/auth/context/AuthContext.tsx create mode 100644 archon-ui-main/src/features/auth/hooks/useAuthQueries.ts create mode 100644 archon-ui-main/src/features/auth/services/authService.ts create mode 100644 archon-ui-main/src/features/auth/types/index.ts create mode 100644 archon-ui-main/src/pages/LoginPage.tsx create mode 100644 archon-ui-main/src/pages/SignUpPage.tsx create mode 100644 python/src/server/api_routes/auth_api.py create mode 100644 python/src/server/middleware/auth_middleware.py create mode 100644 python/src/server/services/auth_service.py diff --git a/.env.example b/.env.example index 363b424..4c2f646 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,13 @@ # https://supabase.com/dashboard/project//settings/api SUPABASE_URL= +# Frontend Supabase Configuration (for authentication) +# These are required for the frontend auth feature +VITE_SUPABASE_URL=${SUPABASE_URL} +# Get the ANON (public) key from Supabase Dashboard > Settings > API +# This is DIFFERENT from the SERVICE_ROLE key - frontend uses ANON key for client-side auth +VITE_SUPABASE_ANON_KEY= + # ⚠️ CRITICAL: You MUST use the SERVICE ROLE key, NOT the Anon key! ⚠️ # # COMMON MISTAKE: Using the anon (public) key will cause ALL saves to fail with "permission denied"! diff --git a/AUTHENTICATION_SETUP.md b/AUTHENTICATION_SETUP.md new file mode 100644 index 0000000..2c02785 --- /dev/null +++ b/AUTHENTICATION_SETUP.md @@ -0,0 +1,283 @@ +# Authentication Setup Guide + +This guide explains how to set up and use Supabase authentication in Archon. + +## Overview + +Archon now supports user authentication using Supabase Auth. This allows you to: +- Create user accounts +- Secure your data with user-specific access +- Protect routes and API endpoints +- Manage sessions and tokens + +## Prerequisites + +1. A Supabase project (create one at [supabase.com](https://supabase.com)) +2. Node.js and Python environment set up + +## Step 1: Configure Environment Variables + +### Backend Configuration + +In your `.env` file, ensure you have: + +```bash +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_KEY=your-service-role-key-here + +# Frontend Supabase Configuration +VITE_SUPABASE_URL=${SUPABASE_URL} +VITE_SUPABASE_ANON_KEY=your-anon-public-key-here +``` + +### Getting Your Keys + +1. Go to your Supabase Dashboard +2. Navigate to **Settings** > **API** +3. Copy the following: + - **Project URL** → `SUPABASE_URL` and `VITE_SUPABASE_URL` + - **anon (public)** key → `VITE_SUPABASE_ANON_KEY` (for frontend) + - **service_role (secret)** key → `SUPABASE_SERVICE_KEY` (for backend) + +**Important:** The frontend uses the ANON key (safe for client-side), while the backend uses the SERVICE_ROLE key (server-side only). + +## Step 2: Enable Email Authentication in Supabase + +1. Go to **Authentication** > **Providers** in your Supabase Dashboard +2. Enable **Email** provider +3. Configure email templates if desired +4. Add allowed redirect URLs: + - Development: `http://localhost:3737/*` + - Production: Your production URL + +## Step 3: Enable Authentication Middleware (Optional) + +By default, the authentication middleware is **disabled** to avoid breaking existing installations. To enable it: + +1. Open `python/src/server/main.py` +2. Find the section labeled "Authentication Middleware (OPTIONAL)" +3. Uncomment the two lines: + ```python + from .middleware.auth_middleware import AuthMiddleware + app.add_middleware(AuthMiddleware) + ``` + +When enabled, all API routes will require authentication except: +- `/health` - Health check +- `/docs`, `/redoc`, `/openapi.json` - API documentation +- `/api/auth/*` - Authentication endpoints + +## Step 4: Start the Application + +```bash +# Frontend +cd archon-ui-main +npm run dev + +# Backend +cd python +uv run python -m src.server.main +``` + +## Step 5: Create Your First User + +1. Navigate to `http://localhost:3737/signup` +2. Enter your email and password +3. You'll be automatically logged in and redirected to the dashboard + +## Features + +### Frontend Features + +- **Login Page** (`/login`) - Sign in with email/password +- **Sign Up Page** (`/signup`) - Create a new account +- **Protected Routes** - All main routes require authentication +- **AuthContext** - Global authentication state management +- **Persistent Sessions** - Sessions saved in localStorage + +### Backend Features + +- **JWT Token Validation** - Verify Supabase JWT tokens +- **Auth Middleware** - Protect API routes automatically +- **User Context** - Access current user in request handlers +- **Auth Service** - Reusable authentication utilities + +## Usage in Components + +### Using Auth Context + +```typescript +import { useAuth } from '@/features/auth/context/AuthContext'; + +function MyComponent() { + const { user, isAuthenticated, isLoading, signOut } = useAuth(); + + if (isLoading) return
Loading...
; + if (!isAuthenticated) return
Not logged in
; + + return ( +
+

Welcome, {user?.email}

+ +
+ ); +} +``` + +### Using TanStack Query Hooks + +```typescript +import { useLoginMutation, useLogoutMutation } from '@/features/auth/hooks/useAuthQueries'; + +function LoginForm() { + const loginMutation = useLoginMutation(); + + const handleLogin = async (email: string, password: string) => { + try { + await loginMutation.mutateAsync({ email, password }); + // Redirect or show success + } catch (error) { + // Handle error + } + }; + + // ... +} +``` + +## Backend API Usage + +### Access Current User in Routes + +```python +from fastapi import Request + +@router.get("/api/my-endpoint") +async def my_endpoint(request: Request): + # User is automatically available if authenticated + user = request.state.user + user_id = user["id"] + user_email = user["email"] + + # Your logic here + return {"user_id": user_id} +``` + +### Validate Token Manually + +```python +from src.server.services.auth_service import auth_service + +async def my_function(token: str): + user = await auth_service.verify_token(token) + return user +``` + +## Row Level Security (RLS) + +To secure your database tables, enable RLS policies in Supabase: + +### Example: Secure `sources` table + +```sql +-- Enable RLS +ALTER TABLE sources ENABLE ROW LEVEL SECURITY; + +-- Policy: Users can only see their own sources +CREATE POLICY "Users can view own sources" +ON sources FOR SELECT +USING (auth.uid() = user_id); + +-- Policy: Users can insert their own sources +CREATE POLICY "Users can insert own sources" +ON sources FOR INSERT +WITH CHECK (auth.uid() = user_id); + +-- Policy: Users can update their own sources +CREATE POLICY "Users can update own sources" +ON sources FOR UPDATE +USING (auth.uid() = user_id); + +-- Policy: Users can delete their own sources +CREATE POLICY "Users can delete own sources" +ON sources FOR DELETE +USING (auth.uid() = user_id); +``` + +Apply similar policies to: +- `documents` +- `archon_projects` +- `archon_tasks` +- Other user-specific tables + +## Troubleshooting + +### "Missing Supabase environment variables" + +- Ensure `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` are set in your `.env` file +- Restart the frontend dev server after adding variables + +### "Authentication failed" errors + +- Check that you're using the correct keys (ANON for frontend, SERVICE_ROLE for backend) +- Verify your Supabase project is active +- Check Supabase logs in the dashboard + +### Users getting 401 on API calls + +- Ensure auth middleware is enabled if you want to protect routes +- Check that the frontend is sending the Authorization header +- Verify the token is valid in Supabase dashboard + +### "Invalid authorization header format" + +- Ensure the header format is: `Authorization: Bearer ` +- Check that the token is being passed correctly from the frontend + +## Security Best Practices + +1. **Never commit `.env` files** - They contain sensitive keys +2. **Use HTTPS in production** - Required for secure authentication +3. **Enable RLS** - Protect your database with Row Level Security +4. **Rotate keys regularly** - Update Supabase keys periodically +5. **Monitor authentication logs** - Check Supabase dashboard for suspicious activity +6. **Set strong password requirements** - Configure in Supabase auth settings + +## Architecture + +``` +Frontend (React) +├── AuthProvider (Context) +├── AuthService (Supabase SDK) +├── ProtectedRoute (Route Guard) +└── Login/Signup Pages + +Backend (FastAPI) +├── AuthMiddleware (JWT Validation) +├── AuthService (Token Verification) +└── Auth API Routes + ├── POST /api/auth/verify + ├── GET /api/auth/user + └── GET /api/auth/health + +Supabase +├── Auth (User Management) +├── Database (PostgreSQL + RLS) +└── JWT Tokens +``` + +## Next Steps + +- Configure email templates in Supabase +- Set up OAuth providers (Google, GitHub, etc.) +- Implement password reset flow +- Add user profile management +- Configure multi-factor authentication (MFA) + +## Support + +For issues or questions: +- Check Supabase documentation: https://supabase.com/docs/guides/auth +- Review Archon's GitHub issues +- Check the CLAUDE.md file for development guidelines diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json index a665375..cae844f 100644 --- a/archon-ui-main/package-lock.json +++ b/archon-ui-main/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.81.1", "@tanstack/react-query": "^5.85.8", "@tanstack/react-query-devtools": "^5.85.8", "clsx": "latest", @@ -3735,6 +3736,85 @@ "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz", + "integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.81.1.tgz", + "integrity": "sha512-sYgSO3mlgL0NvBFS3oRfCK4OgKGQwuOWJLzfPyWg0k8MSxSFSDeN/JtrDJD5GQrxskP6c58+vUzruBJQY78AqQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.81.1.tgz", + "integrity": "sha512-DePpUTAPXJyBurQ4IH2e42DWoA+/Qmr5mbgY4B6ZcxVc/ZUKfTVK31BYIFBATMApWraFc8Q/Sg+yxtfJ3E0wSg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.81.1.tgz", + "integrity": "sha512-ViQ+Kxm8BuUP/TcYmH9tViqYKGSD1LBjdqx2p5J+47RES6c+0QHedM0PPAjthMdAHWyb2LGATE9PD2++2rO/tw==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.1.tgz", + "integrity": "sha512-UNmYtjnZnhouqnbEMC1D5YJot7y0rIaZx7FG2Fv8S3hhNjcGVvO+h9We/tggi273BFkiahQPS/uRsapo1cSapw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.81.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.81.1.tgz", + "integrity": "sha512-KSdY7xb2L0DlLmlYzIOghdw/na4gsMcqJ8u4sD6tOQJr+x3hLujU9s4R8N3ob84/1bkvpvlU5PYKa1ae+OICnw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.81.1", + "@supabase/functions-js": "2.81.1", + "@supabase/postgrest-js": "2.81.1", + "@supabase/realtime-js": "2.81.1", + "@supabase/storage-js": "2.81.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tanstack/query-core": { "version": "5.87.0", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz", @@ -4053,12 +4133,17 @@ "version": "20.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -4098,6 +4183,15 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -11236,7 +11330,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unidiff": { @@ -11962,7 +12055,6 @@ "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/archon-ui-main/package.json b/archon-ui-main/package.json index 31c0757..5b6e2fe 100644 --- a/archon-ui-main/package.json +++ b/archon-ui-main/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.81.1", "@tanstack/react-query": "^5.85.8", "@tanstack/react-query-devtools": "^5.85.8", "clsx": "latest", diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx index ea2539c..530da35 100644 --- a/archon-ui-main/src/App.tsx +++ b/archon-ui-main/src/App.tsx @@ -7,6 +7,8 @@ import { KnowledgeBasePage } from './pages/KnowledgeBasePage'; import { SettingsPage } from './pages/SettingsPage'; import { MCPPage } from './pages/MCPPage'; import { OnboardingPage } from './pages/OnboardingPage'; +import { LoginPage } from './pages/LoginPage'; +import { SignUpPage } from './pages/SignUpPage'; import { MainLayout } from './components/layout/MainLayout'; import { ThemeProvider } from './contexts/ThemeContext'; import { ToastProvider } from './features/ui/components/ToastProvider'; @@ -18,21 +20,25 @@ import { ErrorBoundaryWithBugReport } from './components/bug-report/ErrorBoundar import { MigrationBanner } from './components/ui/MigrationBanner'; import { serverHealthService } from './services/serverHealthService'; import { useMigrationStatus } from './hooks/useMigrationStatus'; +import { AuthProvider } from './features/auth/context/AuthContext'; +import { ProtectedRoute } from './features/auth/components/ProtectedRoute'; const AppRoutes = () => { const { projectsEnabled } = useSettings(); - + return ( - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> {projectsEnabled ? ( <> - } /> - } /> + } /> + } /> ) : ( } /> @@ -111,15 +117,17 @@ const AppContent = () => { export function App() { return ( - - - - - - - - - + + + + + + + + + + + {import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && ( )} diff --git a/archon-ui-main/src/features/auth/components/ProtectedRoute.tsx b/archon-ui-main/src/features/auth/components/ProtectedRoute.tsx new file mode 100644 index 0000000..6bc5ae4 --- /dev/null +++ b/archon-ui-main/src/features/auth/components/ProtectedRoute.tsx @@ -0,0 +1,28 @@ +import { Navigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import type { ReactNode } from "react"; + +interface ProtectedRouteProps { + children: ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} diff --git a/archon-ui-main/src/features/auth/config/supabaseClient.ts b/archon-ui-main/src/features/auth/config/supabaseClient.ts new file mode 100644 index 0000000..1667e0a --- /dev/null +++ b/archon-ui-main/src/features/auth/config/supabaseClient.ts @@ -0,0 +1,19 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error( + "Missing Supabase environment variables. Please set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.", + ); +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + auth: { + persistSession: true, + autoRefreshToken: true, + detectSessionInUrl: true, + storage: window.localStorage, + }, +}); diff --git a/archon-ui-main/src/features/auth/context/AuthContext.tsx b/archon-ui-main/src/features/auth/context/AuthContext.tsx new file mode 100644 index 0000000..1a58c54 --- /dev/null +++ b/archon-ui-main/src/features/auth/context/AuthContext.tsx @@ -0,0 +1,105 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; +import { authService } from "../services/authService"; +import type { AuthState, User, Session } from "../types"; + +interface AuthContextValue extends AuthState { + signIn: (email: string, password: string) => Promise; + signUp: (email: string, password: string, metadata?: Record) => Promise; + signOut: () => Promise; + resetPassword: (email: string) => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + authService + .getSession() + .then((session) => { + setSession(session); + setUser(session?.user ?? null); + }) + .catch((error) => { + console.error("Error loading session:", error); + }) + .finally(() => { + setIsLoading(false); + }); + + const subscription = authService.onAuthStateChange((user, session) => { + setUser(user); + setSession(session); + setIsLoading(false); + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + const signIn = async (email: string, password: string) => { + setIsLoading(true); + try { + const { user, session } = await authService.signIn({ email, password }); + setUser(user); + setSession(session); + } finally { + setIsLoading(false); + } + }; + + const signUp = async (email: string, password: string, metadata?: Record) => { + setIsLoading(true); + try { + const { user, session } = await authService.signUp({ + email, + password, + metadata, + }); + setUser(user); + setSession(session); + } finally { + setIsLoading(false); + } + }; + + const signOut = async () => { + setIsLoading(true); + try { + await authService.signOut(); + setUser(null); + setSession(null); + } finally { + setIsLoading(false); + } + }; + + const resetPassword = async (email: string) => { + await authService.resetPassword(email); + }; + + const value: AuthContextValue = { + user, + session, + isLoading, + isAuthenticated: !!user, + signIn, + signUp, + signOut, + resetPassword, + }; + + return {children}; +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/archon-ui-main/src/features/auth/hooks/useAuthQueries.ts b/archon-ui-main/src/features/auth/hooks/useAuthQueries.ts new file mode 100644 index 0000000..391c64d --- /dev/null +++ b/archon-ui-main/src/features/auth/hooks/useAuthQueries.ts @@ -0,0 +1,67 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { authService } from "../services/authService"; +import { STALE_TIMES } from "../../shared/config/queryPatterns"; +import type { LoginCredentials, SignUpCredentials } from "../types"; + +export const authKeys = { + all: ["auth"] as const, + session: () => [...authKeys.all, "session"] as const, + user: () => [...authKeys.all, "user"] as const, +}; + +export function useAuthSession() { + return useQuery({ + queryKey: authKeys.session(), + queryFn: () => authService.getSession(), + staleTime: STALE_TIMES.rare, + retry: false, + }); +} + +export function useCurrentUser() { + return useQuery({ + queryKey: authKeys.user(), + queryFn: () => authService.getCurrentUser(), + staleTime: STALE_TIMES.rare, + retry: false, + }); +} + +export function useLoginMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (credentials: LoginCredentials) => authService.signIn(credentials), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authKeys.all }); + }, + }); +} + +export function useSignUpMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (credentials: SignUpCredentials) => authService.signUp(credentials), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authKeys.all }); + }, + }); +} + +export function useLogoutMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => authService.signOut(), + onSuccess: () => { + queryClient.clear(); + }, + }); +} + +export function useResetPasswordMutation() { + return useMutation({ + mutationFn: (email: string) => authService.resetPassword(email), + }); +} diff --git a/archon-ui-main/src/features/auth/services/authService.ts b/archon-ui-main/src/features/auth/services/authService.ts new file mode 100644 index 0000000..a95b74b --- /dev/null +++ b/archon-ui-main/src/features/auth/services/authService.ts @@ -0,0 +1,111 @@ +import { supabase } from "../config/supabaseClient"; +import type { LoginCredentials, SignUpCredentials, User, Session } from "../types"; + +export const authService = { + async signIn(credentials: LoginCredentials): Promise<{ user: User; session: Session }> { + const { data, error } = await supabase.auth.signInWithPassword({ + email: credentials.email, + password: credentials.password, + }); + + if (error) { + throw new Error(error.message); + } + + if (!data.user || !data.session) { + throw new Error("Login failed: No user or session returned"); + } + + return { + user: data.user, + session: data.session, + }; + }, + + async signUp(credentials: SignUpCredentials): Promise<{ user: User; session: Session | null }> { + const { data, error } = await supabase.auth.signUp({ + email: credentials.email, + password: credentials.password, + options: { + data: credentials.metadata || {}, + }, + }); + + if (error) { + throw new Error(error.message); + } + + if (!data.user) { + throw new Error("Sign up failed: No user returned"); + } + + return { + user: data.user, + session: data.session, + }; + }, + + async signOut(): Promise { + const { error } = await supabase.auth.signOut(); + + if (error) { + throw new Error(error.message); + } + }, + + async getCurrentUser(): Promise { + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (error) { + throw new Error(error.message); + } + + return user; + }, + + async getSession(): Promise { + const { + data: { session }, + error, + } = await supabase.auth.getSession(); + + if (error) { + throw new Error(error.message); + } + + return session; + }, + + async resetPassword(email: string): Promise { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/reset-password`, + }); + + if (error) { + throw new Error(error.message); + } + }, + + async updatePassword(newPassword: string): Promise { + const { error } = await supabase.auth.updateUser({ + password: newPassword, + }); + + if (error) { + throw new Error(error.message); + } + }, + + onAuthStateChange(callback: (user: User | null, session: Session | null) => void) { + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + callback(session?.user ?? null, session); + }); + + return subscription; + }, +}; diff --git a/archon-ui-main/src/features/auth/types/index.ts b/archon-ui-main/src/features/auth/types/index.ts new file mode 100644 index 0000000..35b9187 --- /dev/null +++ b/archon-ui-main/src/features/auth/types/index.ts @@ -0,0 +1,30 @@ +import type { User as SupabaseUser, Session as SupabaseSession } from "@supabase/supabase-js"; + +export type User = SupabaseUser; +export type Session = SupabaseSession; + +export interface AuthState { + user: User | null; + session: Session | null; + isLoading: boolean; + isAuthenticated: boolean; +} + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface SignUpCredentials { + email: string; + password: string; + metadata?: { + full_name?: string; + [key: string]: unknown; + }; +} + +export interface AuthError { + message: string; + status?: number; +} diff --git a/archon-ui-main/src/pages/LoginPage.tsx b/archon-ui-main/src/pages/LoginPage.tsx new file mode 100644 index 0000000..f77e2a7 --- /dev/null +++ b/archon-ui-main/src/pages/LoginPage.tsx @@ -0,0 +1,93 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useAuth } from "../features/auth/context/AuthContext"; + +export function LoginPage() { + const navigate = useNavigate(); + const { signIn, isLoading } = useAuth(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + try { + await signIn(email, password); + navigate("/"); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred during login"); + } + }; + + return ( +
+
+
+
+

Archon

+

Sign in to your account

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + required + disabled={isLoading} + className="w-full px-4 py-2 bg-gray-900/50 border border-gray-700 rounded-md text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:opacity-50" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + required + disabled={isLoading} + className="w-full px-4 py-2 bg-gray-900/50 border border-gray-700 rounded-md text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:opacity-50" + placeholder="••••••••" + /> +
+ + +
+ +
+

+ Don't have an account?{" "} + + Sign up + +

+
+
+
+
+ ); +} diff --git a/archon-ui-main/src/pages/SignUpPage.tsx b/archon-ui-main/src/pages/SignUpPage.tsx new file mode 100644 index 0000000..82d963e --- /dev/null +++ b/archon-ui-main/src/pages/SignUpPage.tsx @@ -0,0 +1,148 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useAuth } from "../features/auth/context/AuthContext"; + +export function SignUpPage() { + const navigate = useNavigate(); + const { signUp, isLoading } = useAuth(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + if (password.length < 6) { + setError("Password must be at least 6 characters long"); + return; + } + + try { + await signUp(email, password); + setSuccess(true); + setTimeout(() => { + navigate("/"); + }, 2000); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred during sign up"); + } + }; + + if (success) { + return ( +
+
+
+
+ + + +
+

Account Created!

+

Redirecting to your dashboard...

+
+
+
+ ); + } + + return ( +
+
+
+
+

Archon

+

Create your account

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + required + disabled={isLoading} + className="w-full px-4 py-2 bg-gray-900/50 border border-gray-700 rounded-md text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:opacity-50" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + required + disabled={isLoading} + className="w-full px-4 py-2 bg-gray-900/50 border border-gray-700 rounded-md text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:opacity-50" + placeholder="••••••••" + /> +

At least 6 characters

+
+ +
+ + setConfirmPassword(e.target.value)} + required + disabled={isLoading} + className="w-full px-4 py-2 bg-gray-900/50 border border-gray-700 rounded-md text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:opacity-50" + placeholder="••••••••" + /> +
+ + +
+ +
+

+ Already have an account?{" "} + + Sign in + +

+
+
+
+
+ ); +} diff --git a/python/src/server/api_routes/auth_api.py b/python/src/server/api_routes/auth_api.py new file mode 100644 index 0000000..a0e2bca --- /dev/null +++ b/python/src/server/api_routes/auth_api.py @@ -0,0 +1,75 @@ +""" +Authentication API endpoints. + +Provides endpoints for token verification and user information. +Note: Login/signup/logout are handled client-side by Supabase Auth SDK. +""" + +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel + +from ..services.auth_service import auth_service + +router = APIRouter(prefix="/api/auth", tags=["authentication"]) + + +class TokenVerifyRequest(BaseModel): + """Request model for token verification.""" + + token: str + + +class TokenVerifyResponse(BaseModel): + """Response model for token verification.""" + + valid: bool + user: dict | None = None + + +class UserResponse(BaseModel): + """Response model for user information.""" + + id: str + email: str + user_metadata: dict + app_metadata: dict + + +@router.post("/verify", response_model=TokenVerifyResponse) +async def verify_token(request: TokenVerifyRequest): + """ + Verify a JWT token and return user information. + + This endpoint is public and does not require authentication. + It's used to validate tokens from the frontend. + """ + try: + user = await auth_service.verify_token(request.token) + return TokenVerifyResponse(valid=True, user=user) + except HTTPException: + return TokenVerifyResponse(valid=False, user=None) + + +@router.get("/user", response_model=UserResponse) +async def get_current_user(request: Request): + """ + Get information about the currently authenticated user. + + Requires valid JWT token in Authorization header. + """ + if not hasattr(request.state, "user"): + raise HTTPException(status_code=401, detail="Not authenticated") + + user = request.state.user + return UserResponse( + id=user["id"], + email=user["email"], + user_metadata=user.get("user_metadata", {}), + app_metadata=user.get("app_metadata", {}), + ) + + +@router.get("/health") +async def auth_health(): + """Health check endpoint for authentication service.""" + return {"status": "healthy", "service": "auth"} diff --git a/python/src/server/main.py b/python/src/server/main.py index 19456e0..e94cac0 100644 --- a/python/src/server/main.py +++ b/python/src/server/main.py @@ -19,6 +19,7 @@ from fastapi import FastAPI, Response from fastapi.middleware.cors import CORSMiddleware from .api_routes.agent_chat_api import router as agent_chat_router +from .api_routes.auth_api import router as auth_router from .api_routes.bug_report_api import router as bug_report_router from .api_routes.internal_api import router as internal_router from .api_routes.knowledge_api import router as knowledge_router @@ -160,6 +161,11 @@ app.add_middleware( allow_headers=["*"], ) +# Authentication Middleware (OPTIONAL) +# Uncomment the lines below to enable JWT authentication on all routes except public paths +# from .middleware.auth_middleware import AuthMiddleware +# app.add_middleware(AuthMiddleware) + # Add middleware to skip logging for health checks @app.middleware("http") @@ -179,6 +185,7 @@ async def skip_health_check_logs(request, call_next): # Include API routers +app.include_router(auth_router) app.include_router(settings_router) app.include_router(mcp_router) # app.include_router(mcp_client_router) # Removed - not part of new architecture diff --git a/python/src/server/middleware/auth_middleware.py b/python/src/server/middleware/auth_middleware.py new file mode 100644 index 0000000..19853e4 --- /dev/null +++ b/python/src/server/middleware/auth_middleware.py @@ -0,0 +1,48 @@ +""" +Authentication middleware for protecting API routes. +""" + +from fastapi import Request, HTTPException +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import JSONResponse + +from ..services.auth_service import auth_service + + +class AuthMiddleware(BaseHTTPMiddleware): + """ + Middleware to validate JWT tokens on protected routes. + + Public routes that do NOT require authentication: + - /api/auth/* (authentication endpoints) + - /health (health check) + - /docs, /redoc, /openapi.json (API documentation) + """ + + PUBLIC_PATHS = { + "/health", + "/docs", + "/redoc", + "/openapi.json", + } + + PUBLIC_PREFIXES = [ + "/api/auth/", + ] + + async def dispatch(self, request: Request, call_next): + """Process request and validate authentication if required.""" + path = request.url.path + + if path in self.PUBLIC_PATHS or any(path.startswith(prefix) for prefix in self.PUBLIC_PREFIXES): + return await call_next(request) + + try: + user = await auth_service.require_auth(request) + request.state.user = user + except HTTPException as e: + return JSONResponse(status_code=e.status_code, content={"detail": e.detail}) + except Exception as e: + return JSONResponse(status_code=500, content={"detail": f"Authentication error: {str(e)}"}) + + return await call_next(request) diff --git a/python/src/server/services/auth_service.py b/python/src/server/services/auth_service.py new file mode 100644 index 0000000..6c5c314 --- /dev/null +++ b/python/src/server/services/auth_service.py @@ -0,0 +1,97 @@ +""" +Authentication service for validating JWT tokens and managing user sessions. +""" + +from typing import Optional + +from fastapi import HTTPException, Request +from supabase import Client + +from ..utils import get_supabase_client + + +class AuthService: + """Service for handling authentication operations.""" + + def __init__(self): + self.supabase: Client = get_supabase_client() + + async def verify_token(self, token: str) -> dict: + """ + Verify a JWT token and return the user information. + + Args: + token: JWT token from Authorization header + + Returns: + dict: User information from Supabase + + Raises: + HTTPException: If token is invalid or expired + """ + try: + response = self.supabase.auth.get_user(token) + + if not response.user: + raise HTTPException(status_code=401, detail="Invalid authentication token") + + return { + "id": response.user.id, + "email": response.user.email, + "user_metadata": response.user.user_metadata, + "app_metadata": response.user.app_metadata, + } + except Exception as e: + raise HTTPException(status_code=401, detail=f"Authentication failed: {str(e)}") + + async def get_user_by_id(self, user_id: str) -> Optional[dict]: + """ + Get user information by user ID. + + Args: + user_id: User ID from Supabase + + Returns: + dict: User information or None if not found + """ + try: + response = self.supabase.auth.admin.get_user_by_id(user_id) + if not response.user: + return None + + return { + "id": response.user.id, + "email": response.user.email, + "user_metadata": response.user.user_metadata, + "app_metadata": response.user.app_metadata, + } + except Exception: + return None + + async def require_auth(self, request: Request) -> dict: + """ + Extract and verify authentication from request. + + Args: + request: FastAPI request object + + Returns: + dict: User information + + Raises: + HTTPException: If authentication fails + """ + auth_header = request.headers.get("Authorization") + + if not auth_header: + raise HTTPException(status_code=401, detail="Missing authorization header") + + parts = auth_header.split() + if len(parts) != 2 or parts[0].lower() != "bearer": + raise HTTPException(status_code=401, detail="Invalid authorization header format") + + token = parts[1] + return await self.verify_token(token) + + +auth_service = AuthService() From a6b9640738bbc30a84bb45aeb464521f20565cd1 Mon Sep 17 00:00:00 2001 From: Luis Erlacher Date: Sun, 16 Nov 2025 18:07:02 -0300 Subject: [PATCH 3/3] fix: Configure Vite to load environment variables from root directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes authentication environment variable loading by properly configuring Vite's envDir to read from the project root instead of the archon-ui-main directory. Changes: - Configure vite.config.ts with envDir pointing to parent directory - Add explicit VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY to .env - Create vite-env.d.ts for TypeScript environment variable types - Add debug logging to supabaseClient.ts for troubleshooting - Update .env.example with proper Vite variable configuration - Update AUTHENTICATION_SETUP.md with corrected setup instructions Technical details: - Vite's loadEnv() only loads vars for config use, not client injection - envDir config is required to tell Vite where to find .env for client code - Variables must be explicitly defined (Vite doesn't expand ${VAR} syntax) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.example | 16 ++++--- AUTHENTICATION_SETUP.md | 45 ++++++++++--------- archon-ui-main/package-lock.json | 26 ++++++++++- .../features/auth/config/supabaseClient.ts | 13 +++++- archon-ui-main/src/vite-env.d.ts | 14 ++++++ archon-ui-main/vite.config.ts | 7 ++- 6 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 archon-ui-main/src/vite-env.d.ts diff --git a/.env.example b/.env.example index 4c2f646..b32dbb9 100644 --- a/.env.example +++ b/.env.example @@ -5,13 +5,6 @@ # https://supabase.com/dashboard/project//settings/api SUPABASE_URL= -# Frontend Supabase Configuration (for authentication) -# These are required for the frontend auth feature -VITE_SUPABASE_URL=${SUPABASE_URL} -# Get the ANON (public) key from Supabase Dashboard > Settings > API -# This is DIFFERENT from the SERVICE_ROLE key - frontend uses ANON key for client-side auth -VITE_SUPABASE_ANON_KEY= - # ⚠️ CRITICAL: You MUST use the SERVICE ROLE key, NOT the Anon key! ⚠️ # # COMMON MISTAKE: Using the anon (public) key will cause ALL saves to fail with "permission denied"! @@ -29,6 +22,15 @@ VITE_SUPABASE_ANON_KEY= # # On the Supabase dashboard, it's labeled as "service_role" under "Project API keys" SUPABASE_SERVICE_KEY= +SUPABASE_ANON_KEY= + +# Frontend Supabase Configuration (used by Vite) +# These variables are automatically picked up by the frontend build process +# Note: Vite doesn't expand ${VAR} syntax, so these need explicit values (same as above) +# Get the ANON (public) key from Supabase Dashboard > Settings > API +# This is DIFFERENT from the SERVICE_ROLE key - frontend uses ANON key for client-side auth +VITE_SUPABASE_URL= +VITE_SUPABASE_ANON_KEY= # Optional: Set log level for debugging LOGFIRE_TOKEN= diff --git a/AUTHENTICATION_SETUP.md b/AUTHENTICATION_SETUP.md index 2c02785..937c8d1 100644 --- a/AUTHENTICATION_SETUP.md +++ b/AUTHENTICATION_SETUP.md @@ -15,34 +15,37 @@ Archon now supports user authentication using Supabase Auth. This allows you to: 1. A Supabase project (create one at [supabase.com](https://supabase.com)) 2. Node.js and Python environment set up -## Step 1: Configure Environment Variables +## Step 1: Verify Environment Variables -### Backend Configuration - -In your `.env` file, ensure you have: +The authentication system uses the Supabase configuration already present in your root `.env` file: ```bash -# Supabase Configuration -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_SERVICE_KEY=your-service-role-key-here +# Supabase Configuration (Backend) +SUPABASE_URL=https://supabase.automatizase.com.br +SUPABASE_SERVICE_KEY=eyJhbGc... # Your service role key +SUPABASE_ANON_KEY=eyJhbGc... # Your anon/public key -# Frontend Supabase Configuration +# Frontend Supabase Configuration (automatically configured) VITE_SUPABASE_URL=${SUPABASE_URL} -VITE_SUPABASE_ANON_KEY=your-anon-public-key-here +VITE_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY} ``` -### Getting Your Keys +**Important Notes:** +- The frontend uses `VITE_SUPABASE_ANON_KEY` (safe for client-side) +- The backend uses `SUPABASE_SERVICE_KEY` (server-side only) +- These variables are automatically loaded from the root `.env` file +- The Vite config has been updated to read from the root directory -1. Go to your Supabase Dashboard -2. Navigate to **Settings** > **API** -3. Copy the following: - - **Project URL** → `SUPABASE_URL` and `VITE_SUPABASE_URL` - - **anon (public)** key → `VITE_SUPABASE_ANON_KEY` (for frontend) - - **service_role (secret)** key → `SUPABASE_SERVICE_KEY` (for backend) +## Step 2: Install Frontend Dependencies -**Important:** The frontend uses the ANON key (safe for client-side), while the backend uses the SERVICE_ROLE key (server-side only). +```bash +cd archon-ui-main +npm install +``` -## Step 2: Enable Email Authentication in Supabase +This will install the `@supabase/supabase-js` package required for authentication. + +## Step 3: Enable Email Authentication in Supabase 1. Go to **Authentication** > **Providers** in your Supabase Dashboard 2. Enable **Email** provider @@ -51,7 +54,7 @@ VITE_SUPABASE_ANON_KEY=your-anon-public-key-here - Development: `http://localhost:3737/*` - Production: Your production URL -## Step 3: Enable Authentication Middleware (Optional) +## Step 4: Enable Authentication Middleware (Optional) By default, the authentication middleware is **disabled** to avoid breaking existing installations. To enable it: @@ -68,7 +71,7 @@ When enabled, all API routes will require authentication except: - `/docs`, `/redoc`, `/openapi.json` - API documentation - `/api/auth/*` - Authentication endpoints -## Step 4: Start the Application +## Step 5: Start the Application ```bash # Frontend @@ -80,7 +83,7 @@ cd python uv run python -m src.server.main ``` -## Step 5: Create Your First User +## Step 6: Create Your First User 1. Navigate to `http://localhost:3737/signup` 2. Enter your email and password diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json index cae844f..bfea9aa 100644 --- a/archon-ui-main/package-lock.json +++ b/archon-ui-main/package-lock.json @@ -148,6 +148,7 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -881,6 +882,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -969,6 +971,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -978,6 +981,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1152,6 +1156,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1175,6 +1180,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2162,6 +2168,7 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -3840,6 +3847,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz", "integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.87.0" }, @@ -4134,6 +4142,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4155,6 +4164,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4166,6 +4176,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4234,6 +4245,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -4599,6 +4611,7 @@ "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "1.6.1", "fast-glob": "^3.3.2", @@ -4671,6 +4684,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5007,6 +5021,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -6026,6 +6041,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7533,7 +7549,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -7643,6 +7658,7 @@ "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.0.1", "data-urls": "^5.0.0", @@ -7769,7 +7785,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -9615,6 +9630,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9894,6 +9910,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9954,6 +9971,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11212,6 +11230,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11311,6 +11330,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11630,6 +11650,7 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -11713,6 +11734,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", diff --git a/archon-ui-main/src/features/auth/config/supabaseClient.ts b/archon-ui-main/src/features/auth/config/supabaseClient.ts index 1667e0a..26baabf 100644 --- a/archon-ui-main/src/features/auth/config/supabaseClient.ts +++ b/archon-ui-main/src/features/auth/config/supabaseClient.ts @@ -3,9 +3,20 @@ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; +// Debug logging +console.log("🔍 Supabase Config Debug:", { + url: supabaseUrl ? `${supabaseUrl.substring(0, 30)}...` : "MISSING", + anonKey: supabaseAnonKey ? `${supabaseAnonKey.substring(0, 20)}...` : "MISSING", + allEnvVars: Object.keys(import.meta.env).filter(key => key.startsWith('VITE_')) +}); + if (!supabaseUrl || !supabaseAnonKey) { throw new Error( - "Missing Supabase environment variables. Please set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.", + `Missing Supabase environment variables!\n` + + `VITE_SUPABASE_URL: ${supabaseUrl ? 'SET' : 'MISSING'}\n` + + `VITE_SUPABASE_ANON_KEY: ${supabaseAnonKey ? 'SET' : 'MISSING'}\n` + + `Available VITE_ vars: ${Object.keys(import.meta.env).filter(k => k.startsWith('VITE_')).join(', ')}\n` + + `Please ensure .env file is in the root directory (/home/luis/projetos/Archon/.env) and restart the Vite dev server.` ); } diff --git a/archon-ui-main/src/vite-env.d.ts b/archon-ui-main/src/vite-env.d.ts new file mode 100644 index 0000000..bc9b519 --- /dev/null +++ b/archon-ui-main/src/vite-env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + readonly VITE_SUPABASE_URL: string + readonly VITE_SUPABASE_ANON_KEY: string + readonly VITE_HOST?: string + readonly VITE_PORT?: string + readonly VITE_ALLOWED_HOSTS?: string + readonly VITE_SHOW_DEVTOOLS?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/archon-ui-main/vite.config.ts b/archon-ui-main/vite.config.ts index 464f3cf..8185739 100644 --- a/archon-ui-main/vite.config.ts +++ b/archon-ui-main/vite.config.ts @@ -9,8 +9,8 @@ import type { ConfigEnv, UserConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { - // Load environment variables - const env = loadEnv(mode, process.cwd(), ''); + // Load environment variables from root directory (parent of archon-ui-main) + const env = loadEnv(mode, path.resolve(__dirname, '..'), ''); // Get host and port from environment variables or use defaults // For internal Docker communication, use the service name @@ -24,6 +24,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { const port = process.env.ARCHON_SERVER_PORT || env.ARCHON_SERVER_PORT || '8181'; return { + // CRITICAL: Tell Vite where to find .env files (parent directory) + envDir: path.resolve(__dirname, '..'), + plugins: [ react(), // Custom plugin to add test endpoint