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
This commit is contained in:
parent
612d5801de
commit
65348bfed0
@ -5,6 +5,13 @@
|
||||
# https://supabase.com/dashboard/project/<your project ID>/settings/api
|
||||
SUPABASE_URL=
|
||||
|
||||
# Frontend Supabase Configuration (for authentication)
|
||||
# These are required for the frontend auth feature
|
||||
VITE_SUPABASE_URL=${SUPABASE_URL}
|
||||
# Get the ANON (public) key from Supabase Dashboard > Settings > API
|
||||
# This is DIFFERENT from the SERVICE_ROLE key - frontend uses ANON key for client-side auth
|
||||
VITE_SUPABASE_ANON_KEY=
|
||||
|
||||
# ⚠️ CRITICAL: You MUST use the SERVICE ROLE key, NOT the Anon key! ⚠️
|
||||
#
|
||||
# COMMON MISTAKE: Using the anon (public) key will cause ALL saves to fail with "permission denied"!
|
||||
|
||||
283
AUTHENTICATION_SETUP.md
Normal file
283
AUTHENTICATION_SETUP.md
Normal file
@ -0,0 +1,283 @@
|
||||
# Authentication Setup Guide
|
||||
|
||||
This guide explains how to set up and use Supabase authentication in Archon.
|
||||
|
||||
## Overview
|
||||
|
||||
Archon now supports user authentication using Supabase Auth. This allows you to:
|
||||
- Create user accounts
|
||||
- Secure your data with user-specific access
|
||||
- Protect routes and API endpoints
|
||||
- Manage sessions and tokens
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Supabase project (create one at [supabase.com](https://supabase.com))
|
||||
2. Node.js and Python environment set up
|
||||
|
||||
## Step 1: Configure Environment Variables
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
In your `.env` file, ensure you have:
|
||||
|
||||
```bash
|
||||
# Supabase Configuration
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_SERVICE_KEY=your-service-role-key-here
|
||||
|
||||
# Frontend Supabase Configuration
|
||||
VITE_SUPABASE_URL=${SUPABASE_URL}
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-public-key-here
|
||||
```
|
||||
|
||||
### Getting Your Keys
|
||||
|
||||
1. Go to your Supabase Dashboard
|
||||
2. Navigate to **Settings** > **API**
|
||||
3. Copy the following:
|
||||
- **Project URL** → `SUPABASE_URL` and `VITE_SUPABASE_URL`
|
||||
- **anon (public)** key → `VITE_SUPABASE_ANON_KEY` (for frontend)
|
||||
- **service_role (secret)** key → `SUPABASE_SERVICE_KEY` (for backend)
|
||||
|
||||
**Important:** The frontend uses the ANON key (safe for client-side), while the backend uses the SERVICE_ROLE key (server-side only).
|
||||
|
||||
## Step 2: Enable Email Authentication in Supabase
|
||||
|
||||
1. Go to **Authentication** > **Providers** in your Supabase Dashboard
|
||||
2. Enable **Email** provider
|
||||
3. Configure email templates if desired
|
||||
4. Add allowed redirect URLs:
|
||||
- Development: `http://localhost:3737/*`
|
||||
- Production: Your production URL
|
||||
|
||||
## Step 3: Enable Authentication Middleware (Optional)
|
||||
|
||||
By default, the authentication middleware is **disabled** to avoid breaking existing installations. To enable it:
|
||||
|
||||
1. Open `python/src/server/main.py`
|
||||
2. Find the section labeled "Authentication Middleware (OPTIONAL)"
|
||||
3. Uncomment the two lines:
|
||||
```python
|
||||
from .middleware.auth_middleware import AuthMiddleware
|
||||
app.add_middleware(AuthMiddleware)
|
||||
```
|
||||
|
||||
When enabled, all API routes will require authentication except:
|
||||
- `/health` - Health check
|
||||
- `/docs`, `/redoc`, `/openapi.json` - API documentation
|
||||
- `/api/auth/*` - Authentication endpoints
|
||||
|
||||
## Step 4: Start the Application
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
cd archon-ui-main
|
||||
npm run dev
|
||||
|
||||
# Backend
|
||||
cd python
|
||||
uv run python -m src.server.main
|
||||
```
|
||||
|
||||
## Step 5: Create Your First User
|
||||
|
||||
1. Navigate to `http://localhost:3737/signup`
|
||||
2. Enter your email and password
|
||||
3. You'll be automatically logged in and redirected to the dashboard
|
||||
|
||||
## Features
|
||||
|
||||
### Frontend Features
|
||||
|
||||
- **Login Page** (`/login`) - Sign in with email/password
|
||||
- **Sign Up Page** (`/signup`) - Create a new account
|
||||
- **Protected Routes** - All main routes require authentication
|
||||
- **AuthContext** - Global authentication state management
|
||||
- **Persistent Sessions** - Sessions saved in localStorage
|
||||
|
||||
### Backend Features
|
||||
|
||||
- **JWT Token Validation** - Verify Supabase JWT tokens
|
||||
- **Auth Middleware** - Protect API routes automatically
|
||||
- **User Context** - Access current user in request handlers
|
||||
- **Auth Service** - Reusable authentication utilities
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### Using Auth Context
|
||||
|
||||
```typescript
|
||||
import { useAuth } from '@/features/auth/context/AuthContext';
|
||||
|
||||
function MyComponent() {
|
||||
const { user, isAuthenticated, isLoading, signOut } = useAuth();
|
||||
|
||||
if (isLoading) return <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
|
||||
98
archon-ui-main/package-lock.json
generated
98
archon-ui-main/package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"@tanstack/react-query": "^5.85.8",
|
||||
"@tanstack/react-query-devtools": "^5.85.8",
|
||||
"clsx": "latest",
|
||||
@ -3735,6 +3736,85 @@
|
||||
"integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.81.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz",
|
||||
"integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.81.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.81.1.tgz",
|
||||
"integrity": "sha512-sYgSO3mlgL0NvBFS3oRfCK4OgKGQwuOWJLzfPyWg0k8MSxSFSDeN/JtrDJD5GQrxskP6c58+vUzruBJQY78AqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.81.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.81.1.tgz",
|
||||
"integrity": "sha512-DePpUTAPXJyBurQ4IH2e42DWoA+/Qmr5mbgY4B6ZcxVc/ZUKfTVK31BYIFBATMApWraFc8Q/Sg+yxtfJ3E0wSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.81.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.81.1.tgz",
|
||||
"integrity": "sha512-ViQ+Kxm8BuUP/TcYmH9tViqYKGSD1LBjdqx2p5J+47RES6c+0QHedM0PPAjthMdAHWyb2LGATE9PD2++2rO/tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tslib": "2.8.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.81.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.1.tgz",
|
||||
"integrity": "sha512-UNmYtjnZnhouqnbEMC1D5YJot7y0rIaZx7FG2Fv8S3hhNjcGVvO+h9We/tggi273BFkiahQPS/uRsapo1cSapw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.81.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.81.1.tgz",
|
||||
"integrity": "sha512-KSdY7xb2L0DlLmlYzIOghdw/na4gsMcqJ8u4sD6tOQJr+x3hLujU9s4R8N3ob84/1bkvpvlU5PYKa1ae+OICnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.81.1",
|
||||
"@supabase/functions-js": "2.81.1",
|
||||
"@supabase/postgrest-js": "2.81.1",
|
||||
"@supabase/realtime-js": "2.81.1",
|
||||
"@supabase/storage-js": "2.81.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.87.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz",
|
||||
@ -4053,12 +4133,17 @@
|
||||
"version": "20.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
|
||||
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
@ -4098,6 +4183,15 @@
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
@ -11236,7 +11330,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unidiff": {
|
||||
@ -11962,7 +12055,6 @@
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,6 +20,8 @@ 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 = () => {
|
||||
@ -25,14 +29,16 @@ const AppRoutes = () => {
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<KnowledgeBasePage />} />
|
||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/mcp" element={<MCPPage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/signup" element={<SignUpPage />} />
|
||||
<Route path="/" element={<ProtectedRoute><KnowledgeBasePage /></ProtectedRoute>} />
|
||||
<Route path="/onboarding" element={<ProtectedRoute><OnboardingPage /></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
|
||||
<Route path="/mcp" element={<ProtectedRoute><MCPPage /></ProtectedRoute>} />
|
||||
{projectsEnabled ? (
|
||||
<>
|
||||
<Route path="/projects" element={<ProjectPage />} />
|
||||
<Route path="/projects/:projectId" element={<ProjectPage />} />
|
||||
<Route path="/projects" element={<ProtectedRoute><ProjectPage /></ProtectedRoute>} />
|
||||
<Route path="/projects/:projectId" element={<ProtectedRoute><ProjectPage /></ProtectedRoute>} />
|
||||
</>
|
||||
) : (
|
||||
<Route path="/projects" element={<Navigate to="/" replace />} />
|
||||
@ -111,15 +117,17 @@ const AppContent = () => {
|
||||
export function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<TooltipProvider>
|
||||
<SettingsProvider>
|
||||
<AppContent />
|
||||
</SettingsProvider>
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<TooltipProvider>
|
||||
<SettingsProvider>
|
||||
<AppContent />
|
||||
</SettingsProvider>
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
{import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
|
||||
@ -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}</>;
|
||||
}
|
||||
19
archon-ui-main/src/features/auth/config/supabaseClient.ts
Normal file
19
archon-ui-main/src/features/auth/config/supabaseClient.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error(
|
||||
"Missing Supabase environment variables. Please set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.",
|
||||
);
|
||||
}
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
storage: window.localStorage,
|
||||
},
|
||||
});
|
||||
105
archon-ui-main/src/features/auth/context/AuthContext.tsx
Normal file
105
archon-ui-main/src/features/auth/context/AuthContext.tsx
Normal 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;
|
||||
}
|
||||
67
archon-ui-main/src/features/auth/hooks/useAuthQueries.ts
Normal file
67
archon-ui-main/src/features/auth/hooks/useAuthQueries.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
111
archon-ui-main/src/features/auth/services/authService.ts
Normal file
111
archon-ui-main/src/features/auth/services/authService.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
30
archon-ui-main/src/features/auth/types/index.ts
Normal file
30
archon-ui-main/src/features/auth/types/index.ts
Normal 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;
|
||||
}
|
||||
93
archon-ui-main/src/pages/LoginPage.tsx
Normal file
93
archon-ui-main/src/pages/LoginPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
archon-ui-main/src/pages/SignUpPage.tsx
Normal file
148
archon-ui-main/src/pages/SignUpPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
75
python/src/server/api_routes/auth_api.py
Normal file
75
python/src/server/api_routes/auth_api.py
Normal 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"}
|
||||
@ -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
|
||||
|
||||
48
python/src/server/middleware/auth_middleware.py
Normal file
48
python/src/server/middleware/auth_middleware.py
Normal 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)
|
||||
97
python/src/server/services/auth_service.py
Normal file
97
python/src/server/services/auth_service.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user