Merge branch 'test-auth-pr'
Some checks failed
Build Images / build-server-docker (push) Has been cancelled
Build Images / build-mcp-docker (push) Has been cancelled
Build Images / build-agents-docker (push) Has been cancelled
Build Images / build-frontend-docker (push) Has been cancelled
Build Images / build-server-k8s (push) Has been cancelled
Build Images / build-mcp-k8s (push) Has been cancelled
Build Images / build-agents-k8s (push) Has been cancelled
Build Images / build-frontend-k8s (push) Has been cancelled
Some checks failed
Build Images / build-server-docker (push) Has been cancelled
Build Images / build-mcp-docker (push) Has been cancelled
Build Images / build-agents-docker (push) Has been cancelled
Build Images / build-frontend-docker (push) Has been cancelled
Build Images / build-server-k8s (push) Has been cancelled
Build Images / build-mcp-k8s (push) Has been cancelled
Build Images / build-agents-k8s (push) Has been cancelled
Build Images / build-frontend-k8s (push) Has been cancelled
This commit is contained in:
commit
2bc62d0827
@ -22,6 +22,15 @@ SUPABASE_URL=
|
|||||||
#
|
#
|
||||||
# On the Supabase dashboard, it's labeled as "service_role" under "Project API keys"
|
# On the Supabase dashboard, it's labeled as "service_role" under "Project API keys"
|
||||||
SUPABASE_SERVICE_KEY=
|
SUPABASE_SERVICE_KEY=
|
||||||
|
SUPABASE_ANON_KEY=
|
||||||
|
|
||||||
|
# Frontend Supabase Configuration (used by Vite)
|
||||||
|
# These variables are automatically picked up by the frontend build process
|
||||||
|
# Note: Vite doesn't expand ${VAR} syntax, so these need explicit values (same as above)
|
||||||
|
# Get the ANON (public) key from Supabase Dashboard > Settings > API
|
||||||
|
# This is DIFFERENT from the SERVICE_ROLE key - frontend uses ANON key for client-side auth
|
||||||
|
VITE_SUPABASE_URL=
|
||||||
|
VITE_SUPABASE_ANON_KEY=
|
||||||
|
|
||||||
# Optional: Set log level for debugging
|
# Optional: Set log level for debugging
|
||||||
LOGFIRE_TOKEN=
|
LOGFIRE_TOKEN=
|
||||||
|
|||||||
286
AUTHENTICATION_SETUP.md
Normal file
286
AUTHENTICATION_SETUP.md
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
# Authentication Setup Guide
|
||||||
|
|
||||||
|
This guide explains how to set up and use Supabase authentication in Archon.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Archon now supports user authentication using Supabase Auth. This allows you to:
|
||||||
|
- Create user accounts
|
||||||
|
- Secure your data with user-specific access
|
||||||
|
- Protect routes and API endpoints
|
||||||
|
- Manage sessions and tokens
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A Supabase project (create one at [supabase.com](https://supabase.com))
|
||||||
|
2. Node.js and Python environment set up
|
||||||
|
|
||||||
|
## Step 1: Verify Environment Variables
|
||||||
|
|
||||||
|
The authentication system uses the Supabase configuration already present in your root `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase Configuration (Backend)
|
||||||
|
SUPABASE_URL=https://supabase.automatizase.com.br
|
||||||
|
SUPABASE_SERVICE_KEY=eyJhbGc... # Your service role key
|
||||||
|
SUPABASE_ANON_KEY=eyJhbGc... # Your anon/public key
|
||||||
|
|
||||||
|
# Frontend Supabase Configuration (automatically configured)
|
||||||
|
VITE_SUPABASE_URL=${SUPABASE_URL}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- The frontend uses `VITE_SUPABASE_ANON_KEY` (safe for client-side)
|
||||||
|
- The backend uses `SUPABASE_SERVICE_KEY` (server-side only)
|
||||||
|
- These variables are automatically loaded from the root `.env` file
|
||||||
|
- The Vite config has been updated to read from the root directory
|
||||||
|
|
||||||
|
## Step 2: Install Frontend Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd archon-ui-main
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install the `@supabase/supabase-js` package required for authentication.
|
||||||
|
|
||||||
|
## Step 3: Enable Email Authentication in Supabase
|
||||||
|
|
||||||
|
1. Go to **Authentication** > **Providers** in your Supabase Dashboard
|
||||||
|
2. Enable **Email** provider
|
||||||
|
3. Configure email templates if desired
|
||||||
|
4. Add allowed redirect URLs:
|
||||||
|
- Development: `http://localhost:3737/*`
|
||||||
|
- Production: Your production URL
|
||||||
|
|
||||||
|
## Step 4: Enable Authentication Middleware (Optional)
|
||||||
|
|
||||||
|
By default, the authentication middleware is **disabled** to avoid breaking existing installations. To enable it:
|
||||||
|
|
||||||
|
1. Open `python/src/server/main.py`
|
||||||
|
2. Find the section labeled "Authentication Middleware (OPTIONAL)"
|
||||||
|
3. Uncomment the two lines:
|
||||||
|
```python
|
||||||
|
from .middleware.auth_middleware import AuthMiddleware
|
||||||
|
app.add_middleware(AuthMiddleware)
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled, all API routes will require authentication except:
|
||||||
|
- `/health` - Health check
|
||||||
|
- `/docs`, `/redoc`, `/openapi.json` - API documentation
|
||||||
|
- `/api/auth/*` - Authentication endpoints
|
||||||
|
|
||||||
|
## Step 5: Start the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend
|
||||||
|
cd archon-ui-main
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd python
|
||||||
|
uv run python -m src.server.main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Create Your First User
|
||||||
|
|
||||||
|
1. Navigate to `http://localhost:3737/signup`
|
||||||
|
2. Enter your email and password
|
||||||
|
3. You'll be automatically logged in and redirected to the dashboard
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Frontend Features
|
||||||
|
|
||||||
|
- **Login Page** (`/login`) - Sign in with email/password
|
||||||
|
- **Sign Up Page** (`/signup`) - Create a new account
|
||||||
|
- **Protected Routes** - All main routes require authentication
|
||||||
|
- **AuthContext** - Global authentication state management
|
||||||
|
- **Persistent Sessions** - Sessions saved in localStorage
|
||||||
|
|
||||||
|
### Backend Features
|
||||||
|
|
||||||
|
- **JWT Token Validation** - Verify Supabase JWT tokens
|
||||||
|
- **Auth Middleware** - Protect API routes automatically
|
||||||
|
- **User Context** - Access current user in request handlers
|
||||||
|
- **Auth Service** - Reusable authentication utilities
|
||||||
|
|
||||||
|
## Usage in Components
|
||||||
|
|
||||||
|
### Using Auth Context
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useAuth } from '@/features/auth/context/AuthContext';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { user, isAuthenticated, isLoading, signOut } = useAuth();
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (!isAuthenticated) return <div>Not logged in</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Welcome, {user?.email}</p>
|
||||||
|
<button onClick={signOut}>Sign Out</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using TanStack Query Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useLoginMutation, useLogoutMutation } from '@/features/auth/hooks/useAuthQueries';
|
||||||
|
|
||||||
|
function LoginForm() {
|
||||||
|
const loginMutation = useLoginMutation();
|
||||||
|
|
||||||
|
const handleLogin = async (email: string, password: string) => {
|
||||||
|
try {
|
||||||
|
await loginMutation.mutateAsync({ email, password });
|
||||||
|
// Redirect or show success
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend API Usage
|
||||||
|
|
||||||
|
### Access Current User in Routes
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
@router.get("/api/my-endpoint")
|
||||||
|
async def my_endpoint(request: Request):
|
||||||
|
# User is automatically available if authenticated
|
||||||
|
user = request.state.user
|
||||||
|
user_id = user["id"]
|
||||||
|
user_email = user["email"]
|
||||||
|
|
||||||
|
# Your logic here
|
||||||
|
return {"user_id": user_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Token Manually
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.server.services.auth_service import auth_service
|
||||||
|
|
||||||
|
async def my_function(token: str):
|
||||||
|
user = await auth_service.verify_token(token)
|
||||||
|
return user
|
||||||
|
```
|
||||||
|
|
||||||
|
## Row Level Security (RLS)
|
||||||
|
|
||||||
|
To secure your database tables, enable RLS policies in Supabase:
|
||||||
|
|
||||||
|
### Example: Secure `sources` table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE sources ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policy: Users can only see their own sources
|
||||||
|
CREATE POLICY "Users can view own sources"
|
||||||
|
ON sources FOR SELECT
|
||||||
|
USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Policy: Users can insert their own sources
|
||||||
|
CREATE POLICY "Users can insert own sources"
|
||||||
|
ON sources FOR INSERT
|
||||||
|
WITH CHECK (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Policy: Users can update their own sources
|
||||||
|
CREATE POLICY "Users can update own sources"
|
||||||
|
ON sources FOR UPDATE
|
||||||
|
USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Policy: Users can delete their own sources
|
||||||
|
CREATE POLICY "Users can delete own sources"
|
||||||
|
ON sources FOR DELETE
|
||||||
|
USING (auth.uid() = user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply similar policies to:
|
||||||
|
- `documents`
|
||||||
|
- `archon_projects`
|
||||||
|
- `archon_tasks`
|
||||||
|
- Other user-specific tables
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Missing Supabase environment variables"
|
||||||
|
|
||||||
|
- Ensure `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` are set in your `.env` file
|
||||||
|
- Restart the frontend dev server after adding variables
|
||||||
|
|
||||||
|
### "Authentication failed" errors
|
||||||
|
|
||||||
|
- Check that you're using the correct keys (ANON for frontend, SERVICE_ROLE for backend)
|
||||||
|
- Verify your Supabase project is active
|
||||||
|
- Check Supabase logs in the dashboard
|
||||||
|
|
||||||
|
### Users getting 401 on API calls
|
||||||
|
|
||||||
|
- Ensure auth middleware is enabled if you want to protect routes
|
||||||
|
- Check that the frontend is sending the Authorization header
|
||||||
|
- Verify the token is valid in Supabase dashboard
|
||||||
|
|
||||||
|
### "Invalid authorization header format"
|
||||||
|
|
||||||
|
- Ensure the header format is: `Authorization: Bearer <token>`
|
||||||
|
- Check that the token is being passed correctly from the frontend
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never commit `.env` files** - They contain sensitive keys
|
||||||
|
2. **Use HTTPS in production** - Required for secure authentication
|
||||||
|
3. **Enable RLS** - Protect your database with Row Level Security
|
||||||
|
4. **Rotate keys regularly** - Update Supabase keys periodically
|
||||||
|
5. **Monitor authentication logs** - Check Supabase dashboard for suspicious activity
|
||||||
|
6. **Set strong password requirements** - Configure in Supabase auth settings
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend (React)
|
||||||
|
├── AuthProvider (Context)
|
||||||
|
├── AuthService (Supabase SDK)
|
||||||
|
├── ProtectedRoute (Route Guard)
|
||||||
|
└── Login/Signup Pages
|
||||||
|
|
||||||
|
Backend (FastAPI)
|
||||||
|
├── AuthMiddleware (JWT Validation)
|
||||||
|
├── AuthService (Token Verification)
|
||||||
|
└── Auth API Routes
|
||||||
|
├── POST /api/auth/verify
|
||||||
|
├── GET /api/auth/user
|
||||||
|
└── GET /api/auth/health
|
||||||
|
|
||||||
|
Supabase
|
||||||
|
├── Auth (User Management)
|
||||||
|
├── Database (PostgreSQL + RLS)
|
||||||
|
└── JWT Tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Configure email templates in Supabase
|
||||||
|
- Set up OAuth providers (Google, GitHub, etc.)
|
||||||
|
- Implement password reset flow
|
||||||
|
- Add user profile management
|
||||||
|
- Configure multi-factor authentication (MFA)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
- Check Supabase documentation: https://supabase.com/docs/guides/auth
|
||||||
|
- Review Archon's GitHub issues
|
||||||
|
- Check the CLAUDE.md file for development guidelines
|
||||||
124
archon-ui-main/package-lock.json
generated
124
archon-ui-main/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@supabase/supabase-js": "^2.81.1",
|
||||||
"@tanstack/react-query": "^5.85.8",
|
"@tanstack/react-query": "^5.85.8",
|
||||||
"@tanstack/react-query-devtools": "^5.85.8",
|
"@tanstack/react-query-devtools": "^5.85.8",
|
||||||
"clsx": "latest",
|
"clsx": "latest",
|
||||||
@ -147,6 +148,7 @@
|
|||||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@ -880,6 +882,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
|
||||||
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
|
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
"@codemirror/view": "^6.23.0",
|
"@codemirror/view": "^6.23.0",
|
||||||
@ -968,6 +971,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -977,6 +981,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
|
||||||
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
|
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"crelt": "^1.0.6",
|
"crelt": "^1.0.6",
|
||||||
@ -1151,6 +1156,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@ -1174,6 +1180,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@ -2161,6 +2168,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
|
||||||
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.0.0"
|
"@lezer/common": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -3735,6 +3743,85 @@
|
|||||||
"integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
|
"integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@supabase/auth-js": {
|
||||||
|
"version": "2.81.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz",
|
||||||
|
"integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/functions-js": {
|
||||||
|
"version": "2.81.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.81.1.tgz",
|
||||||
|
"integrity": "sha512-sYgSO3mlgL0NvBFS3oRfCK4OgKGQwuOWJLzfPyWg0k8MSxSFSDeN/JtrDJD5GQrxskP6c58+vUzruBJQY78AqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/postgrest-js": {
|
||||||
|
"version": "2.81.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.81.1.tgz",
|
||||||
|
"integrity": "sha512-DePpUTAPXJyBurQ4IH2e42DWoA+/Qmr5mbgY4B6ZcxVc/ZUKfTVK31BYIFBATMApWraFc8Q/Sg+yxtfJ3E0wSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/realtime-js": {
|
||||||
|
"version": "2.81.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.81.1.tgz",
|
||||||
|
"integrity": "sha512-ViQ+Kxm8BuUP/TcYmH9tViqYKGSD1LBjdqx2p5J+47RES6c+0QHedM0PPAjthMdAHWyb2LGATE9PD2++2rO/tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/phoenix": "^1.6.6",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"tslib": "2.8.1",
|
||||||
|
"ws": "^8.18.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/storage-js": {
|
||||||
|
"version": "2.81.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.1.tgz",
|
||||||
|
"integrity": "sha512-UNmYtjnZnhouqnbEMC1D5YJot7y0rIaZx7FG2Fv8S3hhNjcGVvO+h9We/tggi273BFkiahQPS/uRsapo1cSapw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/supabase-js": {
|
||||||
|
"version": "2.81.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.81.1.tgz",
|
||||||
|
"integrity": "sha512-KSdY7xb2L0DlLmlYzIOghdw/na4gsMcqJ8u4sD6tOQJr+x3hLujU9s4R8N3ob84/1bkvpvlU5PYKa1ae+OICnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/auth-js": "2.81.1",
|
||||||
|
"@supabase/functions-js": "2.81.1",
|
||||||
|
"@supabase/postgrest-js": "2.81.1",
|
||||||
|
"@supabase/realtime-js": "2.81.1",
|
||||||
|
"@supabase/storage-js": "2.81.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.87.0",
|
"version": "5.87.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz",
|
||||||
@ -3760,6 +3847,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz",
|
||||||
"integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==",
|
"integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.87.0"
|
"@tanstack/query-core": "5.87.0"
|
||||||
},
|
},
|
||||||
@ -4053,12 +4141,18 @@
|
|||||||
"version": "20.19.0",
|
"version": "20.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
|
||||||
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
|
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/phoenix": {
|
||||||
|
"version": "1.6.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||||
|
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.14",
|
"version": "15.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||||
@ -4070,6 +4164,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
|
||||||
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@ -4081,6 +4176,7 @@
|
|||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
@ -4098,6 +4194,15 @@
|
|||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||||
@ -4140,6 +4245,7 @@
|
|||||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "6.21.0",
|
"@typescript-eslint/scope-manager": "6.21.0",
|
||||||
"@typescript-eslint/types": "6.21.0",
|
"@typescript-eslint/types": "6.21.0",
|
||||||
@ -4505,6 +4611,7 @@
|
|||||||
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
|
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "1.6.1",
|
"@vitest/utils": "1.6.1",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@ -4577,6 +4684,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -4913,6 +5021,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001718",
|
"caniuse-lite": "^1.0.30001718",
|
||||||
"electron-to-chromium": "^1.5.160",
|
"electron-to-chromium": "^1.5.160",
|
||||||
@ -5932,6 +6041,7 @@
|
|||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@ -7439,7 +7549,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||||
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "GitHub Sponsors ❤",
|
"type": "GitHub Sponsors ❤",
|
||||||
"url": "https://github.com/sponsors/dmonad"
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
@ -7549,6 +7658,7 @@
|
|||||||
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
|
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.0.1",
|
"cssstyle": "^4.0.1",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@ -7675,7 +7785,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
|
||||||
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isomorphic.js": "^0.2.4"
|
"isomorphic.js": "^0.2.4"
|
||||||
},
|
},
|
||||||
@ -9521,6 +9630,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -9800,6 +9910,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@ -9860,6 +9971,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@ -11118,6 +11230,7 @@
|
|||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@ -11217,6 +11330,7 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -11236,7 +11350,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unidiff": {
|
"node_modules/unidiff": {
|
||||||
@ -11537,6 +11650,7 @@
|
|||||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@ -11620,6 +11734,7 @@
|
|||||||
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
|
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "1.6.1",
|
"@vitest/expect": "1.6.1",
|
||||||
"@vitest/runner": "1.6.1",
|
"@vitest/runner": "1.6.1",
|
||||||
@ -11962,7 +12077,6 @@
|
|||||||
"version": "8.18.2",
|
"version": "8.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
@ -37,6 +37,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@supabase/supabase-js": "^2.81.1",
|
||||||
"@tanstack/react-query": "^5.85.8",
|
"@tanstack/react-query": "^5.85.8",
|
||||||
"@tanstack/react-query-devtools": "^5.85.8",
|
"@tanstack/react-query-devtools": "^5.85.8",
|
||||||
"clsx": "latest",
|
"clsx": "latest",
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import { KnowledgeBasePage } from './pages/KnowledgeBasePage';
|
|||||||
import { SettingsPage } from './pages/SettingsPage';
|
import { SettingsPage } from './pages/SettingsPage';
|
||||||
import { MCPPage } from './pages/MCPPage';
|
import { MCPPage } from './pages/MCPPage';
|
||||||
import { OnboardingPage } from './pages/OnboardingPage';
|
import { OnboardingPage } from './pages/OnboardingPage';
|
||||||
|
import { LoginPage } from './pages/LoginPage';
|
||||||
|
import { SignUpPage } from './pages/SignUpPage';
|
||||||
import { MainLayout } from './components/layout/MainLayout';
|
import { MainLayout } from './components/layout/MainLayout';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { ToastProvider } from './features/ui/components/ToastProvider';
|
import { ToastProvider } from './features/ui/components/ToastProvider';
|
||||||
@ -18,21 +20,25 @@ import { ErrorBoundaryWithBugReport } from './components/bug-report/ErrorBoundar
|
|||||||
import { MigrationBanner } from './components/ui/MigrationBanner';
|
import { MigrationBanner } from './components/ui/MigrationBanner';
|
||||||
import { serverHealthService } from './services/serverHealthService';
|
import { serverHealthService } from './services/serverHealthService';
|
||||||
import { useMigrationStatus } from './hooks/useMigrationStatus';
|
import { useMigrationStatus } from './hooks/useMigrationStatus';
|
||||||
|
import { AuthProvider } from './features/auth/context/AuthContext';
|
||||||
|
import { ProtectedRoute } from './features/auth/components/ProtectedRoute';
|
||||||
|
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
const { projectsEnabled } = useSettings();
|
const { projectsEnabled } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<KnowledgeBasePage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
<Route path="/signup" element={<SignUpPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/" element={<ProtectedRoute><KnowledgeBasePage /></ProtectedRoute>} />
|
||||||
<Route path="/mcp" element={<MCPPage />} />
|
<Route path="/onboarding" element={<ProtectedRoute><OnboardingPage /></ProtectedRoute>} />
|
||||||
|
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
|
||||||
|
<Route path="/mcp" element={<ProtectedRoute><MCPPage /></ProtectedRoute>} />
|
||||||
{projectsEnabled ? (
|
{projectsEnabled ? (
|
||||||
<>
|
<>
|
||||||
<Route path="/projects" element={<ProjectPage />} />
|
<Route path="/projects" element={<ProtectedRoute><ProjectPage /></ProtectedRoute>} />
|
||||||
<Route path="/projects/:projectId" element={<ProjectPage />} />
|
<Route path="/projects/:projectId" element={<ProtectedRoute><ProjectPage /></ProtectedRoute>} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Route path="/projects" element={<Navigate to="/" replace />} />
|
<Route path="/projects" element={<Navigate to="/" replace />} />
|
||||||
@ -111,15 +117,17 @@ const AppContent = () => {
|
|||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<AuthProvider>
|
||||||
<ToastProvider>
|
<ThemeProvider>
|
||||||
<TooltipProvider>
|
<ToastProvider>
|
||||||
<SettingsProvider>
|
<TooltipProvider>
|
||||||
<AppContent />
|
<SettingsProvider>
|
||||||
</SettingsProvider>
|
<AppContent />
|
||||||
</TooltipProvider>
|
</SettingsProvider>
|
||||||
</ToastProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ToastProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AuthProvider>
|
||||||
{import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && (
|
{import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && (
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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}</>;
|
||||||
|
}
|
||||||
30
archon-ui-main/src/features/auth/config/supabaseClient.ts
Normal file
30
archon-ui-main/src/features/auth/config/supabaseClient.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||||
|
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log("🔍 Supabase Config Debug:", {
|
||||||
|
url: supabaseUrl ? `${supabaseUrl.substring(0, 30)}...` : "MISSING",
|
||||||
|
anonKey: supabaseAnonKey ? `${supabaseAnonKey.substring(0, 20)}...` : "MISSING",
|
||||||
|
allEnvVars: Object.keys(import.meta.env).filter(key => key.startsWith('VITE_'))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing Supabase environment variables!\n` +
|
||||||
|
`VITE_SUPABASE_URL: ${supabaseUrl ? 'SET' : 'MISSING'}\n` +
|
||||||
|
`VITE_SUPABASE_ANON_KEY: ${supabaseAnonKey ? 'SET' : 'MISSING'}\n` +
|
||||||
|
`Available VITE_ vars: ${Object.keys(import.meta.env).filter(k => k.startsWith('VITE_')).join(', ')}\n` +
|
||||||
|
`Please ensure .env file is in the root directory (/home/luis/projetos/Archon/.env) and restart the Vite dev server.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||||
|
auth: {
|
||||||
|
persistSession: true,
|
||||||
|
autoRefreshToken: true,
|
||||||
|
detectSessionInUrl: true,
|
||||||
|
storage: window.localStorage,
|
||||||
|
},
|
||||||
|
});
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
archon-ui-main/src/vite-env.d.ts
vendored
Normal file
14
archon-ui-main/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_SUPABASE_URL: string
|
||||||
|
readonly VITE_SUPABASE_ANON_KEY: string
|
||||||
|
readonly VITE_HOST?: string
|
||||||
|
readonly VITE_PORT?: string
|
||||||
|
readonly VITE_ALLOWED_HOSTS?: string
|
||||||
|
readonly VITE_SHOW_DEVTOOLS?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@ -9,8 +9,8 @@ import type { ConfigEnv, UserConfig } from 'vite';
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||||
// Load environment variables
|
// Load environment variables from root directory (parent of archon-ui-main)
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, path.resolve(__dirname, '..'), '');
|
||||||
|
|
||||||
// Get host and port from environment variables or use defaults
|
// Get host and port from environment variables or use defaults
|
||||||
// For internal Docker communication, use the service name
|
// For internal Docker communication, use the service name
|
||||||
@ -24,6 +24,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
const port = process.env.ARCHON_SERVER_PORT || env.ARCHON_SERVER_PORT || '8181';
|
const port = process.env.ARCHON_SERVER_PORT || env.ARCHON_SERVER_PORT || '8181';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// CRITICAL: Tell Vite where to find .env files (parent directory)
|
||||||
|
envDir: path.resolve(__dirname, '..'),
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
// Custom plugin to add test endpoint
|
// Custom plugin to add test endpoint
|
||||||
|
|||||||
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 fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from .api_routes.agent_chat_api import router as agent_chat_router
|
from .api_routes.agent_chat_api import router as agent_chat_router
|
||||||
|
from .api_routes.auth_api import router as auth_router
|
||||||
from .api_routes.bug_report_api import router as bug_report_router
|
from .api_routes.bug_report_api import router as bug_report_router
|
||||||
from .api_routes.internal_api import router as internal_router
|
from .api_routes.internal_api import router as internal_router
|
||||||
from .api_routes.knowledge_api import router as knowledge_router
|
from .api_routes.knowledge_api import router as knowledge_router
|
||||||
@ -160,6 +161,11 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Authentication Middleware (OPTIONAL)
|
||||||
|
# Uncomment the lines below to enable JWT authentication on all routes except public paths
|
||||||
|
# from .middleware.auth_middleware import AuthMiddleware
|
||||||
|
# app.add_middleware(AuthMiddleware)
|
||||||
|
|
||||||
|
|
||||||
# Add middleware to skip logging for health checks
|
# Add middleware to skip logging for health checks
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
@ -179,6 +185,7 @@ async def skip_health_check_logs(request, call_next):
|
|||||||
|
|
||||||
|
|
||||||
# Include API routers
|
# Include API routers
|
||||||
|
app.include_router(auth_router)
|
||||||
app.include_router(settings_router)
|
app.include_router(settings_router)
|
||||||
app.include_router(mcp_router)
|
app.include_router(mcp_router)
|
||||||
# app.include_router(mcp_client_router) # Removed - not part of new architecture
|
# app.include_router(mcp_client_router) # Removed - not part of new architecture
|
||||||
|
|||||||
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