diff --git a/.env.example b/.env.example
index 363b424..b32dbb9 100644
--- a/.env.example
+++ b/.env.example
@@ -22,6 +22,15 @@ SUPABASE_URL=
#
# 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
new file mode 100644
index 0000000..937c8d1
--- /dev/null
+++ b/AUTHENTICATION_SETUP.md
@@ -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
Loading...
;
+ if (!isAuthenticated) return Not logged in
;
+
+ return (
+
+
Welcome, {user?.email}
+
Sign Out
+
+ );
+}
+```
+
+### 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..bfea9aa 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",
@@ -147,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",
@@ -880,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",
@@ -968,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"
}
@@ -977,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",
@@ -1151,6 +1156,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -1174,6 +1180,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2161,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"
}
@@ -3735,6 +3743,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",
@@ -3760,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"
},
@@ -4053,12 +4141,18 @@
"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",
+ "peer": true,
"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",
@@ -4070,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"
@@ -4081,6 +4176,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -4098,6 +4194,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",
@@ -4140,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",
@@ -4505,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",
@@ -4577,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"
},
@@ -4913,6 +5021,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001718",
"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.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -7439,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"
@@ -7549,6 +7658,7 @@
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssstyle": "^4.0.1",
"data-urls": "^5.0.0",
@@ -7675,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"
},
@@ -9521,6 +9630,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -9800,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"
},
@@ -9860,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"
@@ -11118,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",
@@ -11217,6 +11330,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11236,7 +11350,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": {
@@ -11537,6 +11650,7 @@
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -11620,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",
@@ -11962,7 +12077,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 (
+
+ );
+ }
+
+ 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..26baabf
--- /dev/null
+++ b/archon-ui-main/src/features/auth/config/supabaseClient.ts
@@ -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,
+ },
+});
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
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ Already have an account?{" "}
+
+ Sign in
+
+
+
+
+
+
+ );
+}
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
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()