Compare commits

...

4 Commits

Author SHA1 Message Date
2bc62d0827 Merge branch 'test-auth-pr'
Some checks failed
Build Images / build-server-docker (push) Has been cancelled
Build Images / build-mcp-docker (push) Has been cancelled
Build Images / build-agents-docker (push) Has been cancelled
Build Images / build-frontend-docker (push) Has been cancelled
Build Images / build-server-k8s (push) Has been cancelled
Build Images / build-mcp-k8s (push) Has been cancelled
Build Images / build-agents-k8s (push) Has been cancelled
Build Images / build-frontend-k8s (push) Has been cancelled
2025-11-16 18:07:29 -03:00
a6b9640738 fix: Configure Vite to load environment variables from root directory
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 <noreply@anthropic.com>
2025-11-16 18:07:02 -03:00
Claude
65348bfed0
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
2025-11-15 04:13:17 +00:00
Luis Erlacher
612d5801de 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.
2025-10-10 14:30:15 -03:00
19 changed files with 1297 additions and 23 deletions

View File

@ -22,6 +22,15 @@ SUPABASE_URL=
# #
# On the Supabase dashboard, it's labeled as "service_role" under "Project API keys" # On the Supabase dashboard, it's labeled as "service_role" under "Project API keys"
SUPABASE_SERVICE_KEY= 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 # Optional: Set log level for debugging
LOGFIRE_TOKEN= LOGFIRE_TOKEN=

286
AUTHENTICATION_SETUP.md Normal file
View File

@ -0,0 +1,286 @@
# 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: Verify Environment Variables
The authentication system uses the Supabase configuration already present in your root `.env` file:
```bash
# 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 (automatically configured)
VITE_SUPABASE_URL=${SUPABASE_URL}
VITE_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
```
**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
## Step 2: Install Frontend Dependencies
```bash
cd archon-ui-main
npm install
```
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
3. Configure email templates if desired
4. Add allowed redirect URLs:
- Development: `http://localhost:3737/*`
- Production: Your production URL
## Step 4: 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 5: Start the Application
```bash
# Frontend
cd archon-ui-main
npm run dev
# Backend
cd python
uv run python -m src.server.main
```
## Step 6: 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 <div>Loading...</div>;
if (!isAuthenticated) return <div>Not logged in</div>;
return (
<div>
<p>Welcome, {user?.email}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}
```
### 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 <token>`
- 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

View File

@ -17,6 +17,7 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.81.1",
"@tanstack/react-query": "^5.85.8", "@tanstack/react-query": "^5.85.8",
"@tanstack/react-query-devtools": "^5.85.8", "@tanstack/react-query-devtools": "^5.85.8",
"clsx": "latest", "clsx": "latest",
@ -147,6 +148,7 @@
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@ -880,6 +882,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@ -968,6 +971,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@ -977,6 +981,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@ -1151,6 +1156,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@ -1174,6 +1180,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -2161,6 +2168,7 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@ -3735,6 +3743,85 @@
"integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==", "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
"license": "MIT" "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": { "node_modules/@tanstack/query-core": {
"version": "5.87.0", "version": "5.87.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz",
@ -3760,6 +3847,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz",
"integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==", "integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.87.0" "@tanstack/query-core": "5.87.0"
}, },
@ -4053,12 +4141,18 @@
"version": "20.19.0", "version": "20.19.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "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": { "node_modules/@types/prop-types": {
"version": "15.7.14", "version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@ -4070,6 +4164,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -4081,6 +4176,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
@ -4098,6 +4194,15 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT" "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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
@ -4140,6 +4245,7 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0", "@typescript-eslint/types": "6.21.0",
@ -4505,6 +4611,7 @@
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vitest/utils": "1.6.1", "@vitest/utils": "1.6.1",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@ -4577,6 +4684,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -4913,6 +5021,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001718", "caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160", "electron-to-chromium": "^1.5.160",
@ -5932,6 +6041,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@ -7439,7 +7549,6 @@
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "GitHub Sponsors ❤", "type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
@ -7549,6 +7658,7 @@
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssstyle": "^4.0.1", "cssstyle": "^4.0.1",
"data-urls": "^5.0.0", "data-urls": "^5.0.0",
@ -7675,7 +7785,6 @@
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"isomorphic.js": "^0.2.4" "isomorphic.js": "^0.2.4"
}, },
@ -9521,6 +9630,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -9800,6 +9910,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@ -9860,6 +9971,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@ -11118,6 +11230,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@ -11217,6 +11330,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -11236,7 +11350,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unidiff": { "node_modules/unidiff": {
@ -11537,6 +11650,7 @@
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@ -11620,6 +11734,7 @@
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vitest/expect": "1.6.1", "@vitest/expect": "1.6.1",
"@vitest/runner": "1.6.1", "@vitest/runner": "1.6.1",
@ -11962,7 +12077,6 @@
"version": "8.18.2", "version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@ -37,6 +37,7 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.81.1",
"@tanstack/react-query": "^5.85.8", "@tanstack/react-query": "^5.85.8",
"@tanstack/react-query-devtools": "^5.85.8", "@tanstack/react-query-devtools": "^5.85.8",
"clsx": "latest", "clsx": "latest",

View File

@ -7,6 +7,8 @@ import { KnowledgeBasePage } from './pages/KnowledgeBasePage';
import { SettingsPage } from './pages/SettingsPage'; import { SettingsPage } from './pages/SettingsPage';
import { MCPPage } from './pages/MCPPage'; import { MCPPage } from './pages/MCPPage';
import { OnboardingPage } from './pages/OnboardingPage'; import { OnboardingPage } from './pages/OnboardingPage';
import { LoginPage } from './pages/LoginPage';
import { SignUpPage } from './pages/SignUpPage';
import { MainLayout } from './components/layout/MainLayout'; import { MainLayout } from './components/layout/MainLayout';
import { ThemeProvider } from './contexts/ThemeContext'; import { ThemeProvider } from './contexts/ThemeContext';
import { ToastProvider } from './features/ui/components/ToastProvider'; import { ToastProvider } from './features/ui/components/ToastProvider';
@ -18,6 +20,8 @@ import { ErrorBoundaryWithBugReport } from './components/bug-report/ErrorBoundar
import { MigrationBanner } from './components/ui/MigrationBanner'; import { MigrationBanner } from './components/ui/MigrationBanner';
import { serverHealthService } from './services/serverHealthService'; import { serverHealthService } from './services/serverHealthService';
import { useMigrationStatus } from './hooks/useMigrationStatus'; import { useMigrationStatus } from './hooks/useMigrationStatus';
import { AuthProvider } from './features/auth/context/AuthContext';
import { ProtectedRoute } from './features/auth/components/ProtectedRoute';
const AppRoutes = () => { const AppRoutes = () => {
@ -25,14 +29,16 @@ const AppRoutes = () => {
return ( return (
<Routes> <Routes>
<Route path="/" element={<KnowledgeBasePage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/onboarding" element={<OnboardingPage />} /> <Route path="/signup" element={<SignUpPage />} />
<Route path="/settings" element={<SettingsPage />} /> <Route path="/" element={<ProtectedRoute><KnowledgeBasePage /></ProtectedRoute>} />
<Route path="/mcp" element={<MCPPage />} /> <Route path="/onboarding" element={<ProtectedRoute><OnboardingPage /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
<Route path="/mcp" element={<ProtectedRoute><MCPPage /></ProtectedRoute>} />
{projectsEnabled ? ( {projectsEnabled ? (
<> <>
<Route path="/projects" element={<ProjectPage />} /> <Route path="/projects" element={<ProtectedRoute><ProjectPage /></ProtectedRoute>} />
<Route path="/projects/:projectId" element={<ProjectPage />} /> <Route path="/projects/:projectId" element={<ProtectedRoute><ProjectPage /></ProtectedRoute>} />
</> </>
) : ( ) : (
<Route path="/projects" element={<Navigate to="/" replace />} /> <Route path="/projects" element={<Navigate to="/" replace />} />
@ -111,15 +117,17 @@ const AppContent = () => {
export function App() { export function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider> <AuthProvider>
<ToastProvider> <ThemeProvider>
<TooltipProvider> <ToastProvider>
<SettingsProvider> <TooltipProvider>
<AppContent /> <SettingsProvider>
</SettingsProvider> <AppContent />
</TooltipProvider> </SettingsProvider>
</ToastProvider> </TooltipProvider>
</ThemeProvider> </ToastProvider>
</ThemeProvider>
</AuthProvider>
{import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && ( {import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && (
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
)} )}

View File

@ -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 (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-500 mx-auto" />
<p className="mt-4 text-gray-400">Loading...</p>
</div>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}

View File

@ -0,0 +1,30 @@
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!\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.`
);
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
detectSessionInUrl: true,
storage: window.localStorage,
},
});

View File

@ -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<void>;
signUp: (email: string, password: string, metadata?: Record<string, unknown>) => Promise<void>;
signOut: () => Promise<void>;
resetPassword: (email: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(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<string, unknown>) => {
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

View File

@ -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),
});
}

View File

@ -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<void> {
const { error } = await supabase.auth.signOut();
if (error) {
throw new Error(error.message);
}
},
async getCurrentUser(): Promise<User | null> {
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
throw new Error(error.message);
}
return user;
},
async getSession(): Promise<Session | null> {
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) {
throw new Error(error.message);
}
return session;
},
async resetPassword(email: string): Promise<void> {
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<void> {
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;
},
};

View File

@ -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;
}

View File

@ -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<string | null>(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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-black">
<div className="w-full max-w-md px-6">
<div className="bg-gray-800/50 backdrop-blur-md border border-cyan-500/30 rounded-lg p-8 shadow-2xl">
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold text-cyan-400 mb-2">Archon</h1>
<p className="text-gray-400">Sign in to your account</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-500/10 border border-red-500/50 rounded-md p-3 text-red-400 text-sm">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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="••••••••"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-2 px-4 bg-cyan-600 hover:bg-cyan-700 disabled:bg-cyan-800 disabled:cursor-not-allowed text-white font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 focus:ring-offset-gray-900"
>
{isLoading ? "Signing in..." : "Sign in"}
</button>
</form>
<div className="mt-6 text-center text-sm">
<p className="text-gray-400">
Don't have an account?{" "}
<Link to="/signup" className="text-cyan-400 hover:text-cyan-300 font-medium">
Sign up
</Link>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -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<string | null>(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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-black">
<div className="w-full max-w-md px-6">
<div className="bg-gray-800/50 backdrop-blur-md border border-cyan-500/30 rounded-lg p-8 shadow-2xl text-center">
<div className="mb-4">
<svg
className="w-16 h-16 text-green-500 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-cyan-400 mb-2">Account Created!</h2>
<p className="text-gray-400">Redirecting to your dashboard...</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-black">
<div className="w-full max-w-md px-6">
<div className="bg-gray-800/50 backdrop-blur-md border border-cyan-500/30 rounded-lg p-8 shadow-2xl">
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold text-cyan-400 mb-2">Archon</h1>
<p className="text-gray-400">Create your account</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-500/10 border border-red-500/50 rounded-md p-3 text-red-400 text-sm">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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="••••••••"
/>
<p className="mt-1 text-xs text-gray-500">At least 6 characters</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-300 mb-2">
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => 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="••••••••"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-2 px-4 bg-cyan-600 hover:bg-cyan-700 disabled:bg-cyan-800 disabled:cursor-not-allowed text-white font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 focus:ring-offset-gray-900"
>
{isLoading ? "Creating account..." : "Create account"}
</button>
</form>
<div className="mt-6 text-center text-sm">
<p className="text-gray-400">
Already have an account?{" "}
<Link to="/login" className="text-cyan-400 hover:text-cyan-300 font-medium">
Sign in
</Link>
</p>
</div>
</div>
</div>
</div>
);
}

14
archon-ui-main/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/// <reference types="vite/client" />
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
}

View File

@ -9,8 +9,8 @@ import type { ConfigEnv, UserConfig } from 'vite';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => { export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// Load environment variables // Load environment variables from root directory (parent of archon-ui-main)
const env = loadEnv(mode, process.cwd(), ''); const env = loadEnv(mode, path.resolve(__dirname, '..'), '');
// Get host and port from environment variables or use defaults // Get host and port from environment variables or use defaults
// For internal Docker communication, use the service name // 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'; const port = process.env.ARCHON_SERVER_PORT || env.ARCHON_SERVER_PORT || '8181';
return { return {
// CRITICAL: Tell Vite where to find .env files (parent directory)
envDir: path.resolve(__dirname, '..'),
plugins: [ plugins: [
react(), react(),
// Custom plugin to add test endpoint // Custom plugin to add test endpoint

View File

@ -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"}

View File

@ -19,6 +19,7 @@ from fastapi import FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from .api_routes.agent_chat_api import router as agent_chat_router 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.bug_report_api import router as bug_report_router
from .api_routes.internal_api import router as internal_router from .api_routes.internal_api import router as internal_router
from .api_routes.knowledge_api import router as knowledge_router from .api_routes.knowledge_api import router as knowledge_router
@ -160,6 +161,11 @@ app.add_middleware(
allow_headers=["*"], 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 # Add middleware to skip logging for health checks
@app.middleware("http") @app.middleware("http")
@ -179,6 +185,7 @@ async def skip_health_check_logs(request, call_next):
# Include API routers # Include API routers
app.include_router(auth_router)
app.include_router(settings_router) app.include_router(settings_router)
app.include_router(mcp_router) app.include_router(mcp_router)
# app.include_router(mcp_client_router) # Removed - not part of new architecture # app.include_router(mcp_client_router) # Removed - not part of new architecture

View File

@ -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)

View File

@ -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()