added Quality Audit and fixed various bugs
This commit is contained in:
parent
db62e2d92d
commit
f811854198
25 changed files with 1519 additions and 601 deletions
2
.env
2
.env
|
|
@ -2,7 +2,7 @@ MONGODB_URI=mongodb://localhost:27017
|
|||
MONGODB_DBNAME=agenthub_db
|
||||
SECRET_KEY=your-super-secret-jwt-key-change-in-production-agenthub-2024
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=240
|
||||
|
||||
# Agent Collector API Configuration
|
||||
AGENT_COLLECTOR_API_KEY=agent-collector-static-key-2024-secure
|
||||
|
|
|
|||
98
.gitignore
vendored
98
.gitignore
vendored
|
|
@ -1,3 +1,99 @@
|
|||
# Python
|
||||
venv/
|
||||
|
||||
env/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDEs and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Node.js (if using for frontend build tools)
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Linux
|
||||
*~
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# FastAPI/Uvicorn specific
|
||||
.uvicorn*
|
||||
|
||||
# MongoDB dumps
|
||||
*.dump
|
||||
*.bson
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,310 +0,0 @@
|
|||
# Agent Collector API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Agent Collector is a Flask-based REST API for collecting and storing agent metadata across an organization. The application validates agent data against a predefined JSON schema and stores it in MongoDB for persistence.
|
||||
|
||||
## Base Configuration
|
||||
|
||||
- **Default Host**: `0.0.0.0`
|
||||
- **Default Port**: `8475`
|
||||
- **Server**: Hypercorn ASGI server
|
||||
- **Database**: MongoDB
|
||||
- **Content Type**: `application/json`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MONGO_HOST` | `localhost` | MongoDB host address |
|
||||
| `MONGO_PORT` | `27017` | MongoDB port |
|
||||
| `MONGO_USERNAME` | `admin` | MongoDB username |
|
||||
| `MONGO_PASSWORD` | `admin` | MongoDB password |
|
||||
| `MONGO_DB_NAME` | `agent_collector` | MongoDB database name |
|
||||
| `MONGO_COLLECTION` | `agents` | MongoDB collection name |
|
||||
| `DEBUG` | `False` | Flask debug mode |
|
||||
| `HOST` | `0.0.0.0` | Host to bind to |
|
||||
| `PORT` | `8475` | Port to bind to |
|
||||
| `HYPERCORN_BIND` | `0.0.0.0:8475` | Hypercorn bind address |
|
||||
| `HYPERCORN_WORKERS` | `1` | Number of worker processes |
|
||||
| `HYPERCORN_LOG_LEVEL` | `info` | Logging level |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Health Check
|
||||
|
||||
**Endpoint**: `GET /`
|
||||
|
||||
**Description**: Health check endpoint that verifies the application and MongoDB connection status.
|
||||
|
||||
**Request**: No parameters required
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"message": "Agent collector API is running",
|
||||
"timestamp": "2024-01-01T12:00:00.000000",
|
||||
"database": {
|
||||
"status": "connected",
|
||||
"healthy": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes**:
|
||||
- `200 OK`: Service is healthy
|
||||
|
||||
### Submit Agent Data
|
||||
|
||||
**Endpoint**: `POST /agents`
|
||||
|
||||
**Description**: Collects and validates agent metadata, then stores it in MongoDB.
|
||||
|
||||
**Request Headers**:
|
||||
- `Content-Type: application/json`
|
||||
|
||||
**Request Body Schema**:
|
||||
|
||||
**Required Fields**:
|
||||
- `name` (string): Name of the agent (minimum length: 1)
|
||||
- `description` (string): Detailed description of the agent (minimum length: 1)
|
||||
- `purpose` (string): Primary purpose or function of the agent (minimum length: 1)
|
||||
|
||||
**Optional Fields**:
|
||||
- `location` (string): Physical or virtual location where the agent is deployed
|
||||
- `userbase` (array of strings): List of users or user groups who use this agent
|
||||
- `version` (string): Current version of the agent
|
||||
- `creation_date` (string, ISO 8601 datetime): Date and time when the agent was created
|
||||
- `last_updated` (string, ISO 8601 datetime): Date and time when the agent was last updated
|
||||
- `capabilities` (array of strings): List of agent capabilities
|
||||
- `status` (string): Current operational status - must be one of: `active`, `inactive`, `deprecated`, `development`
|
||||
- `department` (string): Department or team responsible for the agent
|
||||
- `contact_person` (string): Person to contact for issues or inquiries about the agent
|
||||
- `tags` (array of strings): Tags for categorizing the agent
|
||||
- `metadata` (object): Additional arbitrary metadata about the agent
|
||||
|
||||
**Example Request**:
|
||||
```json
|
||||
{
|
||||
"name": "TestAgent",
|
||||
"description": "Test description",
|
||||
"purpose": "Testing purposes",
|
||||
"location": "Development Environment",
|
||||
"userbase": ["dev-team", "qa-team"],
|
||||
"version": "1.0.0",
|
||||
"capabilities": ["data-processing", "automated-testing"],
|
||||
"status": "development",
|
||||
"department": "Engineering",
|
||||
"contact_person": "john.doe@company.com",
|
||||
"tags": ["automation", "testing"],
|
||||
"metadata": {
|
||||
"framework": "custom",
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response** (`201 Created`):
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Agent data collected successfully",
|
||||
"agent_id": "507f1f77bcf86cd799439011"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses**:
|
||||
|
||||
**Unsupported Media Type** (`415`):
|
||||
```json
|
||||
{
|
||||
"error": "Unsupported Media Type",
|
||||
"message": "Request must be JSON"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Error** (`400 Bad Request`):
|
||||
```json
|
||||
{
|
||||
"error": "Invalid Data",
|
||||
"message": "'name' is a required property"
|
||||
}
|
||||
```
|
||||
|
||||
**Database Unavailable** (`503 Service Unavailable`):
|
||||
```json
|
||||
{
|
||||
"error": "Database Unavailable",
|
||||
"message": "MongoDB connection is not available. Please check the database setup.",
|
||||
"agent_data": {
|
||||
// Original submitted data returned for client retry
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Database Error** (`500 Internal Server Error`):
|
||||
```json
|
||||
{
|
||||
"error": "Database Error",
|
||||
"message": "Failed to store agent data. MongoDB may be unavailable or there was an error processing the request.",
|
||||
"agent_data": {
|
||||
// Original submitted data returned for client retry
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Processing
|
||||
|
||||
### Automatic Timestamp Addition
|
||||
|
||||
The API automatically adds timestamps to submitted data:
|
||||
- If `creation_date` is not provided, it's set to the current UTC time
|
||||
- If `last_updated` is not provided, it's set to the current UTC time
|
||||
- Timestamps are in ISO 8601 format
|
||||
|
||||
### Schema Validation
|
||||
|
||||
All submitted data is validated against the JSON schema before storage. The validation:
|
||||
- Ensures required fields are present
|
||||
- Validates data types for all fields
|
||||
- Enforces string length constraints
|
||||
- Validates enum values for status field
|
||||
- Rejects additional properties not defined in the schema
|
||||
|
||||
## Database Operations
|
||||
|
||||
### Connection Management
|
||||
|
||||
- Automatic connection retry with exponential backoff
|
||||
- Connection health checks before operations
|
||||
- Graceful error handling for connection failures
|
||||
- Connection pooling via PyMongo MongoClient
|
||||
|
||||
### Data Storage
|
||||
|
||||
- Each agent record is stored as a MongoDB document
|
||||
- Unique ObjectId is generated for each record
|
||||
- Full document is stored with all provided metadata
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API implements comprehensive error handling:
|
||||
|
||||
1. **Input Validation**: JSON schema validation with detailed error messages
|
||||
2. **Database Connectivity**: Connection retries and graceful degradation
|
||||
3. **Data Preservation**: Failed requests return original data for client retry
|
||||
4. **Logging**: Detailed error logging for troubleshooting
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- No authentication mechanism implemented (authentication should be handled at proxy/gateway level)
|
||||
- Input validation prevents injection attacks
|
||||
- Database credentials configurable via environment variables
|
||||
- Connection timeouts prevent resource exhaustion
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
### Production Deployment
|
||||
|
||||
Run with Hypercorn for production:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
|
||||
For development with Flask's built-in server:
|
||||
```bash
|
||||
python wsgi.py
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
The application is designed to work in containerized environments with:
|
||||
- Environment variable configuration
|
||||
- MongoDB connection timeout handling
|
||||
- Graceful shutdown handling
|
||||
|
||||
## Client Integration
|
||||
|
||||
### Example Client Code (Python)
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Health check
|
||||
response = requests.get('http://localhost:8475/')
|
||||
print(f"Health: {response.json()}")
|
||||
|
||||
# Submit agent data
|
||||
agent_data = {
|
||||
"name": "MyAgent",
|
||||
"description": "Agent for processing customer data",
|
||||
"purpose": "Customer service automation",
|
||||
"status": "active",
|
||||
"department": "Customer Success"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'http://localhost:8475/agents',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps(agent_data)
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
result = response.json()
|
||||
print(f"Agent stored with ID: {result['agent_id']}")
|
||||
else:
|
||||
print(f"Error: {response.json()}")
|
||||
```
|
||||
|
||||
### Example Client Code (curl)
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8475/
|
||||
|
||||
# Submit agent data
|
||||
curl -X POST http://localhost:8475/agents \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "TestAgent",
|
||||
"description": "Test description",
|
||||
"purpose": "Testing purposes"
|
||||
}'
|
||||
```
|
||||
|
||||
## MongoDB Schema
|
||||
|
||||
The MongoDB collection stores documents with the following structure:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"_id": ObjectId("507f1f77bcf86cd799439011"),
|
||||
"name": "AgentName",
|
||||
"description": "Agent description",
|
||||
"purpose": "Agent purpose",
|
||||
"location": "Optional location",
|
||||
"userbase": ["user1", "user2"],
|
||||
"version": "1.0.0",
|
||||
"creation_date": "2024-01-01T12:00:00.000000",
|
||||
"last_updated": "2024-01-01T12:00:00.000000",
|
||||
"capabilities": ["capability1", "capability2"],
|
||||
"status": "active",
|
||||
"department": "Engineering",
|
||||
"contact_person": "contact@company.com",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"metadata": {
|
||||
"custom_field": "custom_value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting and Scaling
|
||||
|
||||
- No built-in rate limiting (should be implemented at proxy/gateway level)
|
||||
- Hypercorn workers can be scaled via `HYPERCORN_WORKERS` environment variable
|
||||
- MongoDB connection pooling handles concurrent requests
|
||||
- Application is stateless and horizontally scalable
|
||||
|
|
@ -5,6 +5,7 @@ load_dotenv()
|
|||
|
||||
# Environment Configuration
|
||||
DISABLE_MSAL = os.getenv("DISABLE_MSAL", "false").lower() == "true"
|
||||
HIDE_LOCAL_LOGIN = os.getenv("HIDE_LOCAL_LOGIN", "false").lower() == "true"
|
||||
BASE_PATH = os.getenv("BASE_PATH", "").rstrip("/") # Remove trailing slash
|
||||
|
||||
def get_base_path() -> str:
|
||||
|
|
@ -22,6 +23,10 @@ def is_msal_enabled() -> bool:
|
|||
"""Check if MSAL authentication is enabled"""
|
||||
return not DISABLE_MSAL
|
||||
|
||||
def show_local_login() -> bool:
|
||||
"""Check if local login should be shown (hidden in production)"""
|
||||
return not HIDE_LOCAL_LOGIN
|
||||
|
||||
def get_msal_config():
|
||||
"""Get MSAL configuration if enabled"""
|
||||
if not is_msal_enabled():
|
||||
|
|
@ -29,6 +34,7 @@ def get_msal_config():
|
|||
|
||||
return {
|
||||
"client_id": os.getenv("AZURE_CLIENT_ID"),
|
||||
"client_secret": os.getenv("AZURE_CLIENT_SECRET"),
|
||||
"authority": os.getenv("AZURE_AUTHORITY"),
|
||||
"redirect_uri": os.getenv("AZURE_REDIRECT_URI"),
|
||||
}
|
||||
53
crud.py
53
crud.py
|
|
@ -137,6 +137,16 @@ async def create_agent(agent_data: dict, user_id: str):
|
|||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
# Initialize quality audit fields if not provided
|
||||
if "quality_audit_status" not in agent_doc:
|
||||
agent_doc["quality_audit_status"] = False
|
||||
if "quality_audit_updated_by" not in agent_doc:
|
||||
agent_doc["quality_audit_updated_by"] = None
|
||||
if "quality_audit_updated_at" not in agent_doc:
|
||||
agent_doc["quality_audit_updated_at"] = None
|
||||
if "quality_audit_updated_by_name" not in agent_doc:
|
||||
agent_doc["quality_audit_updated_by_name"] = None
|
||||
result = await agents_collection.insert_one(agent_doc)
|
||||
agent_doc["_id"] = result.inserted_id
|
||||
return agent_doc
|
||||
|
|
@ -197,12 +207,23 @@ async def get_agent_stats():
|
|||
stats = await agents_collection.aggregate(pipeline).to_list(length=None)
|
||||
return {stat["_id"]: stat["count"] for stat in stats}
|
||||
|
||||
async def update_agent(agent_id: str, update_data: dict, user_id: str = None):
|
||||
async def update_agent(agent_id: str, update_data: dict, user_id: str = None, admin_user_info: dict = None):
|
||||
try:
|
||||
filter_query = {"_id": ObjectId(agent_id)}
|
||||
if user_id:
|
||||
filter_query["created_by"] = user_id
|
||||
|
||||
# Handle Quality Audit updates specially
|
||||
if "quality_audit_status" in update_data and admin_user_info:
|
||||
# Get current agent to check if quality audit status is changing
|
||||
current_agent = await get_agent_by_id(agent_id)
|
||||
|
||||
if current_agent and current_agent.get("quality_audit_status") != update_data["quality_audit_status"]:
|
||||
# Quality audit status is changing, update audit fields with separate timestamp
|
||||
update_data["quality_audit_updated_by"] = admin_user_info.get("user_id")
|
||||
update_data["quality_audit_updated_at"] = datetime.utcnow().isoformat()
|
||||
update_data["quality_audit_updated_by_name"] = admin_user_info.get("user_name", admin_user_info.get("email"))
|
||||
|
||||
update_data["updated_at"] = datetime.utcnow()
|
||||
result = await agents_collection.update_one(
|
||||
filter_query,
|
||||
|
|
@ -214,6 +235,36 @@ async def update_agent(agent_id: str, update_data: dict, user_id: str = None):
|
|||
except:
|
||||
return None
|
||||
|
||||
async def update_agent_quality_audit(agent_id: str, quality_audit_status: bool, admin_user_info: dict):
|
||||
"""Update only the quality audit status of an agent (admin only)"""
|
||||
try:
|
||||
# Get current agent to ensure it exists
|
||||
current_agent = await get_agent_by_id(agent_id)
|
||||
if not current_agent:
|
||||
return None
|
||||
|
||||
# Only update if status is actually changing
|
||||
if current_agent.get("quality_audit_status") == quality_audit_status:
|
||||
return current_agent
|
||||
|
||||
update_data = {
|
||||
"quality_audit_status": quality_audit_status,
|
||||
"quality_audit_updated_by": admin_user_info.get("user_id"),
|
||||
"quality_audit_updated_at": datetime.utcnow().isoformat(),
|
||||
"quality_audit_updated_by_name": admin_user_info.get("user_name", admin_user_info.get("email"))
|
||||
}
|
||||
|
||||
result = await agents_collection.update_one(
|
||||
{"_id": ObjectId(agent_id)},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
if result.modified_count:
|
||||
return await get_agent_by_id(agent_id)
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
async def delete_agent(agent_id: str, user_id: str = None):
|
||||
try:
|
||||
print(f"🗑️ CRUD: Deleting agent {agent_id} with user_id filter: {user_id}")
|
||||
|
|
|
|||
BIN
docs/Agent Tracker API documentation 2025-08-22.pdf
Normal file
BIN
docs/Agent Tracker API documentation 2025-08-22.pdf
Normal file
Binary file not shown.
306
docs/agent_tracker_API_docs.md
Normal file
306
docs/agent_tracker_API_docs.md
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
# Agent Registration API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Agent Registration API allows you to register AI agents and track their usage in the AgentHub system. The API automatically handles duplicate registrations by tracking usage when an agent with the same name already exists.
|
||||
|
||||
**Base URL:** `https://ai-sandbox.oliver.solutions/agent_collector/agents`
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require authentication using a static API key.
|
||||
|
||||
**API Key:** `agent-collector-static-key-2024-secure`
|
||||
|
||||
**Header:** `X-API-Key`
|
||||
|
||||
## Agent Registration Endpoint
|
||||
|
||||
### POST /agents
|
||||
|
||||
Register a new agent or track usage of an existing agent.
|
||||
|
||||
**URL:** `https://ai-sandbox.oliver.solutions/agent_collector/agents`
|
||||
|
||||
**Method:** `POST`
|
||||
|
||||
**Required Headers:**
|
||||
- `Content-Type: application/json`
|
||||
- `X-API-Key: agent-collector-static-key-2024-secure`
|
||||
|
||||
## Request Schema
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | The unique name of the agent (minimum 1 character) |
|
||||
| `description` | string | A detailed description of the agent's functionality (minimum 1 character) |
|
||||
| `purpose` | string | The primary purpose or goal of the agent (minimum 1 character) |
|
||||
| `tool` | string | The tool or platform where the agent operates (minimum 1 character) |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Description | Default |
|
||||
|-------|------|-------------|---------|
|
||||
| `location` | string | Where the agent is deployed or operates | null |
|
||||
| `userbase` | array[string] | List of user groups or departments that use this agent | null |
|
||||
| `version` | string | Version number or identifier of the agent | null |
|
||||
| `creation_date` | string | ISO 8601 datetime when the agent was first created | null |
|
||||
| `last_updated` | string | ISO 8601 datetime when the agent was last modified | null |
|
||||
| `capabilities` | array[string] | List of specific capabilities or features the agent provides | null |
|
||||
| `status` | string | Current operational status of the agent | "development" |
|
||||
| `department` | string | Department or team that owns/manages the agent | null |
|
||||
| `contact_person` | string | Primary contact for questions about this agent | null |
|
||||
| `tags` | array[string] | Keywords or labels for categorizing the agent | null |
|
||||
| `metadata` | object | Additional key-value pairs of agent information | null |
|
||||
|
||||
### Status Values
|
||||
|
||||
The `status` field accepts the following values (case-insensitive):
|
||||
- `"active"` - Agent is live and operational
|
||||
- `"inactive"` - Agent is temporarily disabled
|
||||
- `"deprecated"` - Agent is being phased out
|
||||
- `"development"` - Agent is in development/testing phase (default)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Agent Registration
|
||||
|
||||
**cURL Example:**
|
||||
```bash
|
||||
curl -X POST https://ai-sandbox.oliver.solutions/agent_collector/agents \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: agent-collector-static-key-2024-secure" \
|
||||
-d '{
|
||||
"name": "CustomerSupportBot",
|
||||
"description": "AI assistant that handles customer inquiries and support tickets",
|
||||
"purpose": "Provide 24/7 automated customer support and reduce response times",
|
||||
"tool": "chat-sandbox"
|
||||
}'
|
||||
```
|
||||
|
||||
**Python Example:**
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
url = "https://ai-sandbox.oliver.solutions/agent_collector/agents"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "agent-collector-static-key-2024-secure"
|
||||
}
|
||||
|
||||
data = {
|
||||
"name": "CustomerSupportBot",
|
||||
"description": "AI assistant that handles customer inquiries and support tickets",
|
||||
"purpose": "Provide 24/7 automated customer support and reduce response times",
|
||||
"tool": "chat-sandbox"
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
### Complex Agent Registration
|
||||
|
||||
**cURL Example:**
|
||||
```bash
|
||||
curl -X POST https://ai-sandbox.oliver.solutions/agent_collector/agents \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: agent-collector-static-key-2024-secure" \
|
||||
-d '{
|
||||
"name": "DataAnalysisAgent",
|
||||
"description": "Advanced AI agent that performs automated data analysis and generates business insights",
|
||||
"purpose": "Transform raw business data into actionable insights and recommendations",
|
||||
"tool": "Custom Python Framework",
|
||||
"location": "AWS us-east-1",
|
||||
"userbase": ["Data Science Team", "Business Intelligence", "Executive Team"],
|
||||
"version": "2.1.3",
|
||||
"creation_date": "2024-01-15T10:30:00Z",
|
||||
"last_updated": "2024-08-22T14:45:30Z",
|
||||
"capabilities": ["Data Visualization", "Trend Analysis", "Predictive Modeling", "Report Generation"],
|
||||
"status": "active",
|
||||
"department": "Data Science",
|
||||
"contact_person": "Dr. Sarah Johnson",
|
||||
"tags": ["analytics", "business-intelligence", "automation", "reporting"],
|
||||
"metadata": {
|
||||
"framework": "TensorFlow",
|
||||
"language": "Python",
|
||||
"memory_usage": "2GB",
|
||||
"processing_time": "5-10 minutes"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Python Example:**
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
url = "https://ai-sandbox.oliver.solutions/agent_collector/agents"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "agent-collector-static-key-2024-secure"
|
||||
}
|
||||
|
||||
data = {
|
||||
"name": "DataAnalysisAgent",
|
||||
"description": "Advanced AI agent that performs automated data analysis and generates business insights",
|
||||
"purpose": "Transform raw business data into actionable insights and recommendations",
|
||||
"tool": "Custom Python Framework",
|
||||
"location": "AWS us-east-1",
|
||||
"userbase": ["Data Science Team", "Business Intelligence", "Executive Team"],
|
||||
"version": "2.1.3",
|
||||
"creation_date": "2024-01-15T10:30:00Z",
|
||||
"last_updated": datetime.utcnow().isoformat() + "Z",
|
||||
"capabilities": ["Data Visualization", "Trend Analysis", "Predictive Modeling", "Report Generation"],
|
||||
"status": "active",
|
||||
"department": "Data Science",
|
||||
"contact_person": "Dr. Sarah Johnson",
|
||||
"tags": ["analytics", "business-intelligence", "automation", "reporting"],
|
||||
"metadata": {
|
||||
"framework": "TensorFlow",
|
||||
"language": "Python",
|
||||
"memory_usage": "2GB",
|
||||
"processing_time": "5-10 minutes"
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## Response Formats
|
||||
|
||||
### Successful New Agent Registration
|
||||
|
||||
When a new agent is successfully registered:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Agent data collected successfully",
|
||||
"agent_id": "64f7b8a9e1234567890abcde"
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Tracking Response
|
||||
|
||||
When an agent with the same name already exists, the system logs usage instead of creating a duplicate:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "usage_logged",
|
||||
"message": "Agent already exists, usage tracked",
|
||||
"agent_name": "CustomerSupportBot"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
|
||||
#### Invalid API Key (401)
|
||||
```json
|
||||
{
|
||||
"detail": "Invalid API key"
|
||||
}
|
||||
```
|
||||
|
||||
#### Unsupported Media Type (415)
|
||||
```json
|
||||
{
|
||||
"error": "Unsupported Media Type",
|
||||
"message": "Request must be JSON"
|
||||
}
|
||||
```
|
||||
|
||||
#### Database Error (500)
|
||||
```json
|
||||
{
|
||||
"error": "Database Error",
|
||||
"message": "Failed to store agent data. MongoDB may be unavailable or there was an error processing the request.",
|
||||
"agent_data": {
|
||||
"name": "YourAgentName",
|
||||
"description": "Your agent description",
|
||||
"purpose": "Your agent purpose",
|
||||
"tool": "YourAgentTool"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Database Unavailable (503)
|
||||
```json
|
||||
{
|
||||
"error": "Database Unavailable",
|
||||
"message": "MongoDB connection is not available. Please check the database setup.",
|
||||
"agent_data": {
|
||||
"name": "YourAgentName",
|
||||
"description": "Your agent description",
|
||||
"purpose": "Your agent purpose",
|
||||
"tool": "YourAgentTool"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Tracking Behavior
|
||||
|
||||
The API implements intelligent duplicate detection and usage tracking:
|
||||
|
||||
### New Agent Registration
|
||||
- When you register an agent with a **new name**, the system creates a new agent record
|
||||
- Returns a success response with the new agent's unique ID
|
||||
|
||||
### Usage Tracking
|
||||
- When you register an agent with an **existing name**, the system:
|
||||
- Logs the usage attempt with a timestamp
|
||||
- Compares the submitted data with the existing agent data
|
||||
- Updates the existing agent record if any fields have changed
|
||||
- Returns a usage tracking response
|
||||
|
||||
### Data Updates
|
||||
If usage tracking detects that the submitted agent data differs from the existing record (excluding the name field), the system will automatically update the existing agent with the new information. This ensures agent records stay current while maintaining usage statistics.
|
||||
|
||||
## Date Format
|
||||
|
||||
All date fields (`creation_date`, `last_updated`) should be provided in **ISO 8601 format**:
|
||||
- `"2024-08-22T14:45:30Z"` (with timezone)
|
||||
- `"2024-08-22T14:45:30"` (without timezone)
|
||||
|
||||
## Health Check
|
||||
|
||||
You can check if the API is operational by sending a GET request with JSON Accept header:
|
||||
|
||||
```bash
|
||||
curl -H "Accept: application/json" https://ai-sandbox.oliver.solutions/agent_collector/agents
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"message": "Agent collector API is running",
|
||||
"timestamp": "2024-08-22T14:45:30.123456",
|
||||
"database": {
|
||||
"healthy": true,
|
||||
"connection_status": "connected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use descriptive names**: Agent names should be unique and descriptive to avoid confusion
|
||||
2. **Include version information**: Use the `version` field to track agent iterations
|
||||
3. **Set appropriate status**: Keep the `status` field updated to reflect the agent's current state
|
||||
4. **Provide contact information**: Include a `contact_person` for operational support
|
||||
5. **Use tags effectively**: Tags help with categorization and searching
|
||||
6. **Handle errors gracefully**: Always check response status codes and handle errors appropriately
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Currently, there are no explicit rate limits on the API, but please be considerate of the shared infrastructure and avoid excessive concurrent requests.
|
||||
|
||||
## Support
|
||||
|
||||
For technical support or questions about the Agent Registration API, please contact the AgentHub development team.
|
||||
382
main.py
382
main.py
|
|
@ -71,6 +71,7 @@ def get_template_context(request: Request, current_user=None, **kwargs):
|
|||
"current_user": current_user,
|
||||
"base_path": config.get_base_path(),
|
||||
"msal_enabled": msal_auth.is_msal_available(),
|
||||
"show_local_login": config.show_local_login(),
|
||||
**kwargs
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +118,32 @@ async def require_admin(current_user: dict = Depends(get_current_user_from_cooki
|
|||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
return current_user
|
||||
|
||||
def create_agent_response(agent: dict) -> models.AiAgentResponse:
|
||||
"""Helper function to create AiAgentResponse with all fields including Quality Audit"""
|
||||
return models.AiAgentResponse(
|
||||
agent_id=str(agent["_id"]),
|
||||
agent_name=agent["agent_name"],
|
||||
agent_tool=agent.get("agent_tool"),
|
||||
agent_description=agent.get("agent_description"),
|
||||
agent_purpose=agent.get("agent_purpose"),
|
||||
agent_version=agent.get("agent_version"),
|
||||
agent_status=agent.get("agent_status"),
|
||||
agent_location=agent.get("agent_location"),
|
||||
agent_department=agent.get("agent_department"),
|
||||
agent_contact_person=agent.get("agent_contact_person"),
|
||||
agent_created_at=agent["created_at"].isoformat() if agent.get("created_at") else None,
|
||||
agent_updated_at=agent["updated_at"].isoformat() if agent.get("updated_at") else None,
|
||||
agent_tags=agent.get("agent_tags"),
|
||||
agent_metadata=agent.get("agent_metadata"),
|
||||
agent_userbase=agent.get("agent_userbase"),
|
||||
agent_capabilities=agent.get("agent_capabilities"),
|
||||
quality_audit_status=agent.get("quality_audit_status", False),
|
||||
quality_audit_updated_by=agent.get("quality_audit_updated_by"),
|
||||
quality_audit_updated_at=agent.get("quality_audit_updated_at"),
|
||||
quality_audit_updated_by_name=agent.get("quality_audit_updated_by_name"),
|
||||
created_by=agent["created_by"]
|
||||
)
|
||||
|
||||
async def verify_agent_collector_api_key(x_api_key: str = Header(alias="X-API-Key")):
|
||||
"""Verify static API key for agent collector endpoints"""
|
||||
expected_key = os.getenv("AGENT_COLLECTOR_API_KEY")
|
||||
|
|
@ -141,6 +168,7 @@ def map_agent_collector_to_internal(collector_data: models.AgentCollectorCreate)
|
|||
|
||||
return {
|
||||
"agent_name": collector_data.name,
|
||||
"agent_tool": collector_data.tool,
|
||||
"agent_description": collector_data.description,
|
||||
"agent_purpose": collector_data.purpose,
|
||||
"agent_location": collector_data.location,
|
||||
|
|
@ -230,7 +258,18 @@ async def register_form(
|
|||
@app.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
current_user = await get_current_user_optional(request)
|
||||
return templates.TemplateResponse("login.html", get_template_context(request, current_user))
|
||||
|
||||
# Add MSAL configuration if enabled
|
||||
context = get_template_context(request, current_user)
|
||||
if msal_auth.is_msal_available():
|
||||
msal_config = config.get_msal_config()
|
||||
context.update({
|
||||
"client_id": msal_config["client_id"],
|
||||
"authority": msal_config["authority"],
|
||||
"redirect_uri": msal_config["redirect_uri"]
|
||||
})
|
||||
|
||||
return templates.TemplateResponse("login.html", context)
|
||||
|
||||
@app.post("/login", response_class=HTMLResponse)
|
||||
async def login_form(
|
||||
|
|
@ -279,8 +318,14 @@ async def login_form(
|
|||
# Regular user goes to all agents page
|
||||
response = RedirectResponse(url=get_app_url("agent-management"), status_code=303)
|
||||
|
||||
# Set token in cookie (for demo purposes)
|
||||
response.set_cookie(key="access_token", value=token)
|
||||
# Set token in cookie with proper attributes
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=False # Set to True in production with HTTPS
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -289,79 +334,84 @@ async def login_form(
|
|||
{"request": request, "error": f"Login failed: {str(e)}"}
|
||||
)
|
||||
|
||||
# Azure AD/MSAL Authentication Routes
|
||||
async def azure_login(request: Request):
|
||||
"""Initiate Azure AD login with PKCE"""
|
||||
try:
|
||||
msal_instance = msal_auth.get_msal_instance()
|
||||
auth_data = msal_instance.get_auth_url()
|
||||
|
||||
# Store PKCE parameters in session
|
||||
request.session["msal_state"] = auth_data["state"]
|
||||
request.session["msal_code_verifier"] = auth_data["code_verifier"]
|
||||
|
||||
# Redirect to Azure AD
|
||||
return RedirectResponse(url=auth_data["auth_url"], status_code=302)
|
||||
|
||||
except Exception as e:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": f"Azure login failed: {str(e)}"}
|
||||
)
|
||||
# Azure AD/MSAL Authentication - Using popup-based authentication per specification
|
||||
|
||||
async def azure_callback(request: Request):
|
||||
"""Handle Azure AD callback and complete authentication"""
|
||||
|
||||
@app.post("/api/auth/azure/token")
|
||||
async def azure_token_exchange(request: Request):
|
||||
"""Exchange Azure AD token for local JWT token with proper validation"""
|
||||
try:
|
||||
# Get authorization code and state from callback
|
||||
auth_code = request.query_params.get("code")
|
||||
state = request.query_params.get("state")
|
||||
error = request.query_params.get("error")
|
||||
data = await request.json()
|
||||
access_token = data.get("access_token")
|
||||
id_token = data.get("id_token")
|
||||
|
||||
if error:
|
||||
error_description = request.query_params.get("error_description", "Unknown error")
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": f"Azure AD error: {error_description}"}
|
||||
if not id_token:
|
||||
raise HTTPException(status_code=400, detail="Missing ID token")
|
||||
|
||||
# Validate JWT token against Azure AD public keys (as per specification)
|
||||
import jwt
|
||||
import requests
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
import base64
|
||||
|
||||
msal_config = config.get_msal_config()
|
||||
|
||||
# Get Azure AD public keys for token verification
|
||||
jwks_uri = f"{msal_config['authority']}/discovery/v2.0/keys"
|
||||
jwks_response = requests.get(jwks_uri)
|
||||
jwks = jwks_response.json()
|
||||
|
||||
# Decode token header to get key ID
|
||||
unverified_header = jwt.get_unverified_header(id_token)
|
||||
kid = unverified_header.get("kid")
|
||||
|
||||
# Find the matching public key
|
||||
public_key = None
|
||||
for key in jwks["keys"]:
|
||||
if key["kid"] == kid:
|
||||
# Convert JWK to PEM format for validation
|
||||
n = base64.urlsafe_b64decode(key["n"] + "==")
|
||||
e = base64.urlsafe_b64decode(key["e"] + "==")
|
||||
|
||||
# Convert to RSA public key
|
||||
numbers = rsa.RSAPublicNumbers(
|
||||
int.from_bytes(e, byteorder="big"),
|
||||
int.from_bytes(n, byteorder="big")
|
||||
)
|
||||
public_key = numbers.public_key()
|
||||
break
|
||||
|
||||
if not public_key:
|
||||
raise HTTPException(status_code=400, detail="Unable to verify token signature")
|
||||
|
||||
# Convert to PEM format for JWT library
|
||||
pem_key = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
# Validate JWT token signature and claims
|
||||
try:
|
||||
id_token_claims = jwt.decode(
|
||||
id_token,
|
||||
pem_key,
|
||||
algorithms=["RS256"],
|
||||
audience=msal_config["client_id"],
|
||||
issuer=f"{msal_config['authority']}/v2.0"
|
||||
)
|
||||
except jwt.InvalidTokenError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Token validation failed: {str(e)}")
|
||||
|
||||
if not auth_code:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "No authorization code received from Azure AD"}
|
||||
)
|
||||
|
||||
# Validate state parameter (CSRF protection)
|
||||
session_state = request.session.get("msal_state")
|
||||
code_verifier = request.session.get("msal_code_verifier")
|
||||
|
||||
if not session_state or not code_verifier:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "Session expired. Please try logging in again."}
|
||||
)
|
||||
|
||||
msal_instance = msal_auth.get_msal_instance()
|
||||
if not msal_instance.validate_state(state, session_state):
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "Invalid state parameter. Possible CSRF attack."}
|
||||
)
|
||||
|
||||
# Exchange authorization code for tokens
|
||||
token_result = msal_instance.acquire_token_by_auth_code(auth_code, code_verifier)
|
||||
if not token_result:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "Failed to acquire token from Azure AD"}
|
||||
)
|
||||
|
||||
# Extract user profile
|
||||
user_profile = msal_instance.get_user_profile(token_result)
|
||||
if not user_profile:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "Failed to get user profile from Azure AD"}
|
||||
)
|
||||
# Create user profile from validated ID token claims
|
||||
user_profile = {
|
||||
"azure_ad_id": id_token_claims.get("oid"),
|
||||
"email": id_token_claims.get("email") or id_token_claims.get("preferred_username"),
|
||||
"full_name": id_token_claims.get("name"),
|
||||
"first_name": id_token_claims.get("given_name"),
|
||||
"last_name": id_token_claims.get("family_name"),
|
||||
"tenant_id": id_token_claims.get("tid")
|
||||
}
|
||||
|
||||
# Create or update user in local database
|
||||
user = await crud.create_or_update_azure_user(user_profile)
|
||||
|
|
@ -369,29 +419,30 @@ async def azure_callback(request: Request):
|
|||
# Create JWT token for local session management
|
||||
jwt_token = auth.create_access_token({"sub": str(user["_id"])})
|
||||
|
||||
# Clear MSAL session data
|
||||
request.session.pop("msal_state", None)
|
||||
request.session.pop("msal_code_verifier", None)
|
||||
# Return user info and set secure cookie (following specification)
|
||||
response = JSONResponse({
|
||||
"success": True,
|
||||
"is_admin": user.get("is_admin", False),
|
||||
"email": user.get("email"),
|
||||
"full_name": user.get("full_name")
|
||||
})
|
||||
|
||||
# Redirect based on user role
|
||||
if user.get("is_admin"):
|
||||
response = RedirectResponse(url=get_app_url("admin"), status_code=303)
|
||||
else:
|
||||
response = RedirectResponse(url=get_app_url("agent-management"), status_code=303)
|
||||
# Set JWT token in httpOnly cookie with security flags (as per specification)
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=jwt_token,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=False, # Set to True in production with HTTPS
|
||||
max_age=24 * 60 * 60 # 24 hours as per specification
|
||||
)
|
||||
|
||||
# Set JWT token in cookie
|
||||
response.set_cookie(key="access_token", value=jwt_token)
|
||||
return response
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Clear session on error
|
||||
request.session.pop("msal_state", None)
|
||||
request.session.pop("msal_code_verifier", None)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": f"Authentication failed: {str(e)}"}
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Token exchange failed: {str(e)}")
|
||||
|
||||
@app.get("/agent-register", response_class=HTMLResponse)
|
||||
async def agent_register_page(request: Request):
|
||||
|
|
@ -404,6 +455,7 @@ async def agent_register_page(request: Request):
|
|||
async def agent_register_form(
|
||||
request: Request,
|
||||
agent_name: str = Form(...),
|
||||
agent_tool: str = Form(...),
|
||||
agent_description: str = Form(None),
|
||||
agent_purpose: str = Form(None),
|
||||
agent_version: str = Form(None),
|
||||
|
|
@ -413,7 +465,8 @@ async def agent_register_form(
|
|||
agent_contact_person: str = Form(None),
|
||||
agent_tags: str = Form(None),
|
||||
agent_userbase: str = Form(None),
|
||||
agent_capabilities: str = Form(None)
|
||||
agent_capabilities: str = Form(None),
|
||||
quality_audit_status: bool = Form(False)
|
||||
):
|
||||
try:
|
||||
# Get user from cookie - require authentication
|
||||
|
|
@ -426,6 +479,7 @@ async def agent_register_form(
|
|||
# Prepare agent data
|
||||
agent_data = {
|
||||
"agent_name": agent_name,
|
||||
"agent_tool": agent_tool,
|
||||
"agent_description": agent_description,
|
||||
"agent_purpose": agent_purpose,
|
||||
"agent_version": agent_version,
|
||||
|
|
@ -443,6 +497,18 @@ async def agent_register_form(
|
|||
if agent_capabilities:
|
||||
agent_data["agent_capabilities"] = [cap.strip() for cap in agent_capabilities.split(',') if cap.strip()]
|
||||
|
||||
# Handle Quality Audit - only admins can set it to True
|
||||
if quality_audit_status and current_user.get("is_admin"):
|
||||
# Admin is setting quality audit to true
|
||||
from datetime import datetime
|
||||
agent_data["quality_audit_status"] = True
|
||||
agent_data["quality_audit_updated_by"] = user_id
|
||||
agent_data["quality_audit_updated_at"] = datetime.utcnow().isoformat()
|
||||
agent_data["quality_audit_updated_by_name"] = current_user.get("full_name", current_user.get("email"))
|
||||
else:
|
||||
# Non-admin or quality audit not checked
|
||||
agent_data["quality_audit_status"] = False
|
||||
|
||||
# Remove None values
|
||||
agent_data = {k: v for k, v in agent_data.items() if v is not None}
|
||||
|
||||
|
|
@ -635,77 +701,23 @@ async def get_all_agents_for_users(current_user: dict = Depends(get_current_user
|
|||
"""Get all agents for regular users (read-only access)"""
|
||||
agents = await crud.get_all_agents()
|
||||
return [
|
||||
models.AiAgentResponse(
|
||||
agent_id=str(agent["_id"]),
|
||||
agent_name=agent["agent_name"],
|
||||
agent_description=agent.get("agent_description"),
|
||||
agent_purpose=agent.get("agent_purpose"),
|
||||
agent_version=agent.get("agent_version"),
|
||||
agent_status=agent.get("agent_status"),
|
||||
agent_location=agent.get("agent_location"),
|
||||
agent_department=agent.get("agent_department"),
|
||||
agent_contact_person=agent.get("agent_contact_person"),
|
||||
agent_created_at=agent["created_at"].isoformat() if agent.get("created_at") else None,
|
||||
agent_updated_at=agent["updated_at"].isoformat() if agent.get("updated_at") else None,
|
||||
agent_tags=agent.get("agent_tags"),
|
||||
agent_metadata=agent.get("agent_metadata"),
|
||||
agent_userbase=agent.get("agent_userbase"),
|
||||
agent_capabilities=agent.get("agent_capabilities"),
|
||||
created_by=agent["created_by"]
|
||||
) for agent in agents
|
||||
create_agent_response(agent) for agent in agents
|
||||
]
|
||||
|
||||
@app.post("/api/agents", response_model=models.AiAgentResponse)
|
||||
async def create_agent(agent: models.AiAgentCreate, current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
agent_data = agent.model_dump()
|
||||
created_agent = await crud.create_agent(agent_data, str(current_user["_id"]))
|
||||
return models.AiAgentResponse(
|
||||
agent_id=str(created_agent["_id"]),
|
||||
agent_name=created_agent["agent_name"],
|
||||
agent_description=created_agent.get("agent_description"),
|
||||
agent_purpose=created_agent.get("agent_purpose"),
|
||||
agent_version=created_agent.get("agent_version"),
|
||||
agent_status=created_agent.get("agent_status"),
|
||||
agent_location=created_agent.get("agent_location"),
|
||||
agent_department=created_agent.get("agent_department"),
|
||||
agent_contact_person=created_agent.get("agent_contact_person"),
|
||||
agent_created_at=created_agent["created_at"].isoformat(),
|
||||
agent_updated_at=created_agent["updated_at"].isoformat(),
|
||||
agent_tags=created_agent.get("agent_tags"),
|
||||
agent_metadata=created_agent.get("agent_metadata"),
|
||||
agent_userbase=created_agent.get("agent_userbase"),
|
||||
agent_capabilities=created_agent.get("agent_capabilities"),
|
||||
created_by=created_agent["created_by"]
|
||||
)
|
||||
return create_agent_response(created_agent)
|
||||
|
||||
@app.get("/api/agents", response_model=List[models.AiAgentResponse])
|
||||
async def get_user_agents(current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
agents = await crud.get_agents_by_user(str(current_user["_id"]))
|
||||
return [
|
||||
models.AiAgentResponse(
|
||||
agent_id=str(agent["_id"]),
|
||||
agent_name=agent["agent_name"],
|
||||
agent_description=agent.get("agent_description"),
|
||||
agent_purpose=agent.get("agent_purpose"),
|
||||
agent_version=agent.get("agent_version"),
|
||||
agent_status=agent.get("agent_status"),
|
||||
agent_location=agent.get("agent_location"),
|
||||
agent_department=agent.get("agent_department"),
|
||||
agent_contact_person=agent.get("agent_contact_person"),
|
||||
agent_created_at=agent["created_at"].isoformat() if agent.get("created_at") else None,
|
||||
agent_updated_at=agent["updated_at"].isoformat() if agent.get("updated_at") else None,
|
||||
agent_tags=agent.get("agent_tags"),
|
||||
agent_metadata=agent.get("agent_metadata"),
|
||||
agent_userbase=agent.get("agent_userbase"),
|
||||
agent_capabilities=agent.get("agent_capabilities"),
|
||||
created_by=agent["created_by"]
|
||||
) for agent in agents
|
||||
create_agent_response(agent) for agent in agents
|
||||
]
|
||||
|
||||
# Conditionally register MSAL routes if available
|
||||
if msal_auth.is_msal_available():
|
||||
app.get("/auth/azure/login")(azure_login)
|
||||
app.get("/auth/azure/callback")(azure_callback)
|
||||
# MSAL routes no longer needed - using popup authentication
|
||||
|
||||
@app.get("/api/agents/{agent_id}", response_model=models.AiAgentResponse)
|
||||
async def get_agent(agent_id: str, current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
|
|
@ -716,23 +728,7 @@ async def get_agent(agent_id: str, current_user: dict = Depends(get_current_user
|
|||
if agent["created_by"] != str(current_user["_id"]) and not current_user.get("is_admin"):
|
||||
raise HTTPException(status_code=403, detail="Not authorized to view this agent")
|
||||
|
||||
return models.AiAgentResponse(
|
||||
agent_id=str(agent["_id"]),
|
||||
agent_name=agent["agent_name"],
|
||||
agent_description=agent.get("agent_description"),
|
||||
agent_purpose=agent.get("agent_purpose"),
|
||||
agent_version=agent.get("agent_version"),
|
||||
agent_status=agent.get("agent_status"),
|
||||
agent_location=agent.get("agent_location"),
|
||||
agent_department=agent.get("agent_department"),
|
||||
agent_contact_person=agent.get("agent_contact_person"),
|
||||
agent_created_at=agent["created_at"].isoformat() if agent.get("created_at") else None,
|
||||
agent_updated_at=agent["updated_at"].isoformat() if agent.get("updated_at") else None,
|
||||
agent_tags=agent.get("agent_tags"),
|
||||
agent_metadata=agent.get("agent_metadata"),
|
||||
agent_userbase=agent.get("agent_userbase"),
|
||||
created_by=agent["created_by"]
|
||||
)
|
||||
return create_agent_response(agent)
|
||||
|
||||
@app.put("/api/agents/{agent_id}", response_model=models.AiAgentResponse)
|
||||
async def update_agent(agent_id: str, agent: models.AiAgentCreate, current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
|
|
@ -748,28 +744,24 @@ async def update_agent(agent_id: str, agent: models.AiAgentCreate, current_user:
|
|||
# For regular users, pass user_id to enforce ownership at DB level
|
||||
# For admins, pass None to allow editing any agent
|
||||
user_id_filter = str(current_user["_id"]) if not current_user.get("is_admin") else None
|
||||
updated_agent = await crud.update_agent(agent_id, agent.model_dump(), user_id_filter)
|
||||
|
||||
# Prepare admin info for Quality Audit updates
|
||||
admin_user_info = None
|
||||
agent_data = agent.model_dump()
|
||||
|
||||
if current_user.get("is_admin"):
|
||||
admin_user_info = {
|
||||
"user_id": str(current_user["_id"]),
|
||||
"user_name": current_user.get("full_name", current_user.get("email")),
|
||||
"email": current_user.get("email")
|
||||
}
|
||||
|
||||
updated_agent = await crud.update_agent(agent_id, agent_data, user_id_filter, admin_user_info)
|
||||
|
||||
if not updated_agent:
|
||||
raise HTTPException(status_code=404, detail="Agent not found or not authorized")
|
||||
|
||||
return models.AiAgentResponse(
|
||||
agent_id=str(updated_agent["_id"]),
|
||||
agent_name=updated_agent["agent_name"],
|
||||
agent_description=updated_agent.get("agent_description"),
|
||||
agent_purpose=updated_agent.get("agent_purpose"),
|
||||
agent_version=updated_agent.get("agent_version"),
|
||||
agent_status=updated_agent.get("agent_status"),
|
||||
agent_location=updated_agent.get("agent_location"),
|
||||
agent_department=updated_agent.get("agent_department"),
|
||||
agent_contact_person=updated_agent.get("agent_contact_person"),
|
||||
agent_created_at=updated_agent["created_at"].isoformat() if updated_agent.get("created_at") else None,
|
||||
agent_updated_at=updated_agent["updated_at"].isoformat() if updated_agent.get("updated_at") else None,
|
||||
agent_tags=updated_agent.get("agent_tags"),
|
||||
agent_metadata=updated_agent.get("agent_metadata"),
|
||||
agent_userbase=updated_agent.get("agent_userbase"),
|
||||
created_by=updated_agent["created_by"]
|
||||
)
|
||||
return create_agent_response(updated_agent)
|
||||
|
||||
@app.delete("/api/agents/{agent_id}")
|
||||
async def delete_agent(agent_id: str, current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
|
|
@ -837,24 +829,7 @@ async def get_all_agents_admin(current_user: dict = Depends(require_admin)):
|
|||
|
||||
agents = await crud.get_all_agents()
|
||||
return [
|
||||
models.AiAgentResponse(
|
||||
agent_id=str(agent["_id"]),
|
||||
agent_name=agent["agent_name"],
|
||||
agent_description=agent.get("agent_description"),
|
||||
agent_purpose=agent.get("agent_purpose"),
|
||||
agent_version=agent.get("agent_version"),
|
||||
agent_status=agent.get("agent_status"),
|
||||
agent_location=agent.get("agent_location"),
|
||||
agent_department=agent.get("agent_department"),
|
||||
agent_contact_person=agent.get("agent_contact_person"),
|
||||
agent_created_at=agent["created_at"].isoformat() if agent.get("created_at") else None,
|
||||
agent_updated_at=agent["updated_at"].isoformat() if agent.get("updated_at") else None,
|
||||
agent_tags=agent.get("agent_tags"),
|
||||
agent_metadata=agent.get("agent_metadata"),
|
||||
agent_userbase=agent.get("agent_userbase"),
|
||||
agent_capabilities=agent.get("agent_capabilities"),
|
||||
created_by=agent["created_by"]
|
||||
) for agent in agents
|
||||
create_agent_response(agent) for agent in agents
|
||||
]
|
||||
|
||||
# Agent Collector API Endpoints (for compatibility with agent_collector app)
|
||||
|
|
@ -1007,7 +982,4 @@ async def get_agent_usage_chart(
|
|||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get chart data: {str(e)}")
|
||||
|
||||
# Conditionally register MSAL routes if available
|
||||
if msal_auth.is_msal_available():
|
||||
app.get("/auth/azure/login")(azure_login)
|
||||
app.get("/auth/azure/callback")(azure_callback)
|
||||
# MSAL routes no longer needed - using popup authentication
|
||||
|
|
|
|||
16
models.py
16
models.py
|
|
@ -5,6 +5,7 @@ from typing import Optional
|
|||
class AiAgent(BaseModel):
|
||||
agent_id: int
|
||||
agent_name: str
|
||||
agent_tool: str | None = Field(default=None, title="The tool or platform where the agent operates", max_length=100)
|
||||
agent_description: str | None = Field(default=None, title="The description of the agent", max_length=300)
|
||||
agent_purpose: str | None = Field(default=None, title="The purpose of the agent", max_length=200)
|
||||
agent_version: str | None = Field(default=None, title="The version of the agent", max_length=100)
|
||||
|
|
@ -18,6 +19,10 @@ class AiAgent(BaseModel):
|
|||
agent_metadata: dict[str, str] | None = Field(default=None, title="Metadata associated with the agent")
|
||||
agent_userbase: list[str] | None = Field(default=None, title="Userbase associated with the agent")
|
||||
agent_capabilities: list[str] | None = Field(default=None, title="Capabilities of the agent")
|
||||
quality_audit_status: bool | None = Field(default=False, title="Quality audit status")
|
||||
quality_audit_updated_by: str | None = Field(default=None, title="Admin user ID who updated quality audit")
|
||||
quality_audit_updated_at: str | None = Field(default=None, title="Quality audit last update timestamp")
|
||||
quality_audit_updated_by_name: str | None = Field(default=None, title="Admin user name who updated quality audit")
|
||||
|
||||
|
||||
|
||||
|
|
@ -50,6 +55,7 @@ class Token(BaseModel):
|
|||
# Agent models for creation and response
|
||||
class AiAgentCreate(BaseModel):
|
||||
agent_name: str
|
||||
agent_tool: str
|
||||
agent_description: Optional[str] = None
|
||||
agent_purpose: Optional[str] = None
|
||||
agent_version: Optional[str] = None
|
||||
|
|
@ -61,10 +67,15 @@ class AiAgentCreate(BaseModel):
|
|||
agent_metadata: Optional[dict[str, str]] = None
|
||||
agent_userbase: Optional[list[str]] = None
|
||||
agent_capabilities: Optional[list[str]] = None
|
||||
quality_audit_status: Optional[bool] = False
|
||||
quality_audit_updated_by: Optional[str] = None
|
||||
quality_audit_updated_at: Optional[str] = None
|
||||
quality_audit_updated_by_name: Optional[str] = None
|
||||
|
||||
class AiAgentResponse(BaseModel):
|
||||
agent_id: str
|
||||
agent_name: str
|
||||
agent_tool: Optional[str] = None
|
||||
agent_description: Optional[str] = None
|
||||
agent_purpose: Optional[str] = None
|
||||
agent_version: Optional[str] = None
|
||||
|
|
@ -78,6 +89,10 @@ class AiAgentResponse(BaseModel):
|
|||
agent_metadata: Optional[dict[str, str]] = None
|
||||
agent_userbase: Optional[list[str]] = None
|
||||
agent_capabilities: Optional[list[str]] = None
|
||||
quality_audit_status: Optional[bool] = None
|
||||
quality_audit_updated_by: Optional[str] = None
|
||||
quality_audit_updated_at: Optional[str] = None
|
||||
quality_audit_updated_by_name: Optional[str] = None
|
||||
created_by: str
|
||||
|
||||
# Agent Collector API Models (for compatibility with agent_collector app)
|
||||
|
|
@ -85,6 +100,7 @@ class AgentCollectorCreate(BaseModel):
|
|||
name: str = Field(min_length=1)
|
||||
description: str = Field(min_length=1)
|
||||
purpose: str = Field(min_length=1)
|
||||
tool: str = Field(min_length=1)
|
||||
location: Optional[str] = None
|
||||
userbase: Optional[list[str]] = None
|
||||
version: Optional[str] = None
|
||||
|
|
|
|||
70
msal_auth.py
70
msal_auth.py
|
|
@ -11,8 +11,8 @@ if config.is_msal_enabled():
|
|||
else:
|
||||
msal = None
|
||||
|
||||
# MSAL Scopes
|
||||
SCOPES = ["User.Read"]
|
||||
# MSAL Scopes - Following specification
|
||||
SCOPES = ["openid", "profile", "email"]
|
||||
|
||||
class MSALAuth:
|
||||
def __init__(self):
|
||||
|
|
@ -36,7 +36,7 @@ class MSALAuth:
|
|||
raise ValueError("Missing Azure AD configuration. Check environment variables.")
|
||||
|
||||
def _build_msal_app(self, cache=None):
|
||||
"""Create MSAL confidential client application"""
|
||||
"""Create MSAL public client application for SPA"""
|
||||
return msal.PublicClientApplication(
|
||||
self.client_id,
|
||||
authority=self.authority,
|
||||
|
|
@ -52,7 +52,7 @@ class MSALAuth:
|
|||
|
||||
def get_auth_url(self, session_state: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate authorization URL with PKCE challenge
|
||||
Generate authorization URL with PKCE challenge for SPA
|
||||
Returns: dict with auth_url, state, and code_verifier for session storage
|
||||
"""
|
||||
if not self.enabled:
|
||||
|
|
@ -60,19 +60,30 @@ class MSALAuth:
|
|||
|
||||
app = self._build_msal_app()
|
||||
|
||||
# Generate PKCE parameters
|
||||
# Generate PKCE parameters (required for SPA)
|
||||
code_verifier = secrets.token_urlsafe(96)
|
||||
|
||||
# Generate state parameter for CSRF protection
|
||||
state = session_state or secrets.token_urlsafe(32)
|
||||
|
||||
auth_url = app.get_authorization_request_url(
|
||||
scopes=SCOPES,
|
||||
redirect_uri=self.redirect_uri,
|
||||
state=state,
|
||||
code_challenge=self._generate_pkce_challenge(code_verifier),
|
||||
code_challenge_method="S256"
|
||||
)
|
||||
code_challenge = self._generate_pkce_challenge(code_verifier)
|
||||
|
||||
# Manual URL construction to ensure PKCE parameters are included
|
||||
# MSAL library sometimes doesn't include PKCE params correctly for SPA
|
||||
import urllib.parse
|
||||
|
||||
auth_params = {
|
||||
"client_id": self.client_id,
|
||||
"response_type": "code",
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"scope": " ".join(SCOPES),
|
||||
"state": state,
|
||||
"code_challenge": code_challenge,
|
||||
"code_challenge_method": "S256",
|
||||
"response_mode": "query"
|
||||
}
|
||||
|
||||
auth_url = f"{self.authority}/oauth2/v2.0/authorize?" + urllib.parse.urlencode(auth_params)
|
||||
|
||||
return {
|
||||
"auth_url": auth_url,
|
||||
|
|
@ -85,22 +96,43 @@ class MSALAuth:
|
|||
code_verifier: str,
|
||||
scopes: Optional[list] = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Exchange authorization code for tokens using PKCE
|
||||
Exchange authorization code for tokens using PKCE for SPA
|
||||
"""
|
||||
if not self.enabled:
|
||||
raise RuntimeError("MSAL is disabled")
|
||||
|
||||
if not code_verifier:
|
||||
print("MSAL Error: code_verifier is required for SPA")
|
||||
return None
|
||||
|
||||
app = self._build_msal_app()
|
||||
|
||||
if scopes is None:
|
||||
scopes = SCOPES
|
||||
|
||||
result = app.acquire_token_by_authorization_code(
|
||||
auth_code,
|
||||
scopes=scopes,
|
||||
redirect_uri=self.redirect_uri,
|
||||
code_verifier=code_verifier
|
||||
)
|
||||
# Manual token request for PKCE flow - MSAL library may not support code_verifier properly
|
||||
import requests
|
||||
|
||||
token_url = f"{self.authority}/oauth2/v2.0/token"
|
||||
token_data = {
|
||||
"client_id": self.client_id,
|
||||
"scope": " ".join(scopes),
|
||||
"code": auth_code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
"code_verifier": code_verifier
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(token_url, data=token_data)
|
||||
result = response.json()
|
||||
|
||||
if response.status_code != 200:
|
||||
result["error"] = result.get("error", "token_request_failed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"MSAL Error: Manual token request failed: {e}")
|
||||
return None
|
||||
|
||||
if "error" in result:
|
||||
print(f"MSAL Error: {result.get('error_description', result.get('error'))}")
|
||||
|
|
|
|||
224
msal_pkce_flow.md
Normal file
224
msal_pkce_flow.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# MSAL PKCE Authentication Flow Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document specifies the Microsoft Authentication Library (MSAL) implementation with Proof Key for Code Exchange (PKCE) flow used in this web application. The implementation uses popup-based authentication with Azure AD/Entra ID and includes server-side JWT validation with httpOnly cookies for secure session management.
|
||||
|
||||
## Architecture Components
|
||||
|
||||
### Frontend Components
|
||||
- **MSAL Browser Library**: `msal-browser.min.js` v2.15.0
|
||||
- **Authentication Method**: Popup-based login with PKCE
|
||||
- **Token Storage**: Session storage for MSAL tokens, httpOnly cookies for server sessions
|
||||
- **Cache Configuration**: Session storage with fallback cookie for auth state
|
||||
|
||||
### Backend Components
|
||||
- **Server-Side Validation**: Custom JWT validation using Firebase JWT library
|
||||
- **Session Management**: httpOnly cookies with security headers
|
||||
- **Token Validation**: Real-time validation against Azure AD public keys
|
||||
- **Authentication Middleware**: Server-side protection for all routes
|
||||
|
||||
## Azure AD Configuration
|
||||
|
||||
### Application Registration
|
||||
- **Client ID**: `9079054c-9620-4757-a256-23413042f1ef`
|
||||
- **Tenant ID**: `e519c2e6-bc6d-4fdf-8d9c-923c2f002385`
|
||||
- **Authority URL**: `https://login.microsoftonline.com/{tenantId}`
|
||||
- **Application Type**: Single Page Application (SPA)
|
||||
- **Authentication Flow**: Authorization Code with PKCE
|
||||
- **Redirect URI**: Dynamic based on application origin
|
||||
|
||||
### Required Scopes
|
||||
- `openid` - OpenID Connect authentication
|
||||
- `profile` - User profile information
|
||||
- `email` - User email address
|
||||
|
||||
## MSAL Configuration
|
||||
|
||||
### Client Configuration
|
||||
```javascript
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: "9079054c-9620-4757-a256-23413042f1ef",
|
||||
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
|
||||
redirectUri: window.location.origin + window.location.pathname.replace(/\/$/, '')
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "sessionStorage",
|
||||
storeAuthStateInCookie: true,
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Login Request Configuration
|
||||
```javascript
|
||||
const loginRequest = {
|
||||
scopes: ["openid", "profile", "email"],
|
||||
prompt: "select_account"
|
||||
};
|
||||
```
|
||||
|
||||
## Authentication Flow Steps
|
||||
|
||||
### 1. User Access Control
|
||||
- **Server-Side Gate**: All pages require authentication via `AuthMiddleware->requireAuth()`
|
||||
- **Unauthenticated Users**: Redirected to login interface automatically
|
||||
- **Token Validation**: Every request validates JWT against Azure AD public keys
|
||||
|
||||
### 2. Login Initiation
|
||||
1. User clicks "Sign In with Microsoft" button
|
||||
2. `myMSALObj.loginPopup(loginRequest)` triggers PKCE flow
|
||||
3. MSAL automatically generates PKCE code verifier and challenge
|
||||
4. User redirected to Azure AD login in popup window
|
||||
|
||||
### 3. Azure AD Authentication
|
||||
1. User authenticates with Microsoft credentials
|
||||
2. Azure AD validates user and application permissions
|
||||
3. Authorization code returned with PKCE validation
|
||||
4. MSAL exchanges code for tokens using PKCE code verifier
|
||||
|
||||
### 4. Token Exchange and Validation
|
||||
1. **Client receives tokens**: ID token and access token from MSAL
|
||||
2. **Server submission**: Tokens sent to `auth.php` endpoint
|
||||
3. **JWT validation**: Server validates token signature against Azure AD JWKS
|
||||
4. **Claims validation**: Audience, issuer, expiration, and timing claims checked
|
||||
5. **Cookie creation**: Valid tokens stored in httpOnly cookie with security flags
|
||||
|
||||
### 5. Session Management
|
||||
1. **httpOnly cookie**: Prevents JavaScript access to authentication token
|
||||
2. **Security flags**: Secure, SameSite=Lax, path=/, 24-hour expiration
|
||||
3. **Server-side validation**: Every request validates cookie token
|
||||
4. **Automatic renewal**: No automatic token refresh implemented
|
||||
|
||||
### 6. Logout Process
|
||||
1. **Client logout**: `auth.php` called to clear server-side cookie
|
||||
2. **MSAL logout**: `myMSALObj.logoutPopup()` clears client-side tokens
|
||||
3. **Page reload**: Fresh page load to reflect authentication state
|
||||
|
||||
## Security Implementation
|
||||
|
||||
### Token Security
|
||||
- **httpOnly cookies**: Prevent XSS access to authentication tokens
|
||||
- **Secure flag**: Cookies only sent over HTTPS in production
|
||||
- **SameSite protection**: CSRF protection with Lax policy
|
||||
- **Server-side validation**: No client-side security dependencies
|
||||
|
||||
### JWT Validation Process
|
||||
1. **JWKS retrieval**: Public keys fetched from Azure AD OpenID configuration
|
||||
2. **Signature verification**: JWT signature validated using RS256 algorithm
|
||||
3. **Claims validation**: Comprehensive validation of all standard claims
|
||||
4. **Real-time validation**: Every request validates token freshness
|
||||
|
||||
### Supported Token Types
|
||||
- **Primary**: ID tokens (preferred for user authentication)
|
||||
- **Fallback**: Access tokens (Microsoft Graph audience)
|
||||
- **Validation**: Both token types supported with appropriate audience validation
|
||||
|
||||
## Server-Side Components
|
||||
|
||||
### AuthMiddleware.php
|
||||
- **Purpose**: Authentication gate for all protected resources
|
||||
- **Methods**:
|
||||
- `requireAuth()`: Enforce authentication for page access
|
||||
- `isAuthenticated()`: Check current authentication status
|
||||
- `setAuthToken()`: Store validated token in httpOnly cookie
|
||||
- `clearAuthToken()`: Remove authentication cookie
|
||||
|
||||
### JWTValidator.php
|
||||
- **Purpose**: Validate Azure AD JWT tokens server-side
|
||||
- **Features**:
|
||||
- Real-time JWKS key retrieval from Azure AD
|
||||
- Comprehensive claims validation (exp, nbf, aud, iss)
|
||||
- Support for multiple token audiences and issuers
|
||||
- Firebase JWT library integration
|
||||
|
||||
### auth.php
|
||||
- **Purpose**: Authentication endpoint for login/logout operations
|
||||
- **Endpoints**:
|
||||
- `POST /auth.php?action=login`: Process authentication tokens
|
||||
- `POST /auth.php?action=logout`: Clear authentication session
|
||||
- `GET /auth.php?action=status`: Check authentication status
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Authentication Errors
|
||||
- **Invalid tokens**: Clear error messages with retry options
|
||||
- **Expired sessions**: Automatic redirect to login interface
|
||||
- **Network errors**: Graceful fallback with user notifications
|
||||
- **Popup blocking**: Error handling for blocked popup windows
|
||||
|
||||
### Server-Side Error Handling
|
||||
- **Token validation failures**: Specific error messages logged and returned
|
||||
- **JWKS retrieval errors**: Fallback mechanisms and retry logic
|
||||
- **Cookie security**: Proper error responses for cookie failures
|
||||
|
||||
## Implementation Requirements
|
||||
|
||||
### Frontend Dependencies
|
||||
- MSAL Browser library v2.15.0 or later
|
||||
- Modern browser with popup support
|
||||
- JavaScript ES6+ support
|
||||
- Fetch API for token submission
|
||||
|
||||
### Backend Dependencies
|
||||
- PHP 7.4+ with OpenSSL support
|
||||
- Firebase JWT library via Composer
|
||||
- cURL or file_get_contents for HTTPS requests
|
||||
- Cookie support in server environment
|
||||
|
||||
### Azure AD Requirements
|
||||
- Application registered as Single Page Application
|
||||
- Redirect URIs configured for all deployment environments
|
||||
- Appropriate API permissions granted
|
||||
- PKCE support enabled (default for SPAs)
|
||||
|
||||
## Configuration Variables
|
||||
|
||||
### Required Environment Configuration
|
||||
```php
|
||||
// AuthMiddleware.php
|
||||
private $tenantId = 'e519c2e6-bc6d-4fdf-8d9c-923c2f002385';
|
||||
private $clientId = '9079054c-9620-4757-a256-23413042f1ef';
|
||||
```
|
||||
|
||||
### Cookie Security Configuration
|
||||
```php
|
||||
$cookieOptions = [
|
||||
'expires' => time() + (24 * 60 * 60), // 24 hours
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => isset($_SERVER['HTTPS']), // HTTPS in production
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
```
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Authentication Testing
|
||||
1. **Successful login**: Verify popup authentication and cookie creation
|
||||
2. **Token validation**: Confirm server-side JWT validation
|
||||
3. **Session persistence**: Test page navigation with authentication
|
||||
4. **Logout functionality**: Verify complete session termination
|
||||
|
||||
### Security Testing
|
||||
1. **Cookie security**: Verify httpOnly and secure flags
|
||||
2. **XSS protection**: Confirm no client-side token exposure
|
||||
3. **Token expiration**: Test automatic session invalidation
|
||||
4. **CSRF protection**: Verify SameSite cookie protection
|
||||
|
||||
## Migration Considerations
|
||||
|
||||
### From Basic MSAL Implementation
|
||||
1. **Add server-side validation**: Implement JWT validation middleware
|
||||
2. **Replace client-side token storage**: Move to httpOnly cookies
|
||||
3. **Add authentication gates**: Protect all server endpoints
|
||||
4. **Update error handling**: Implement proper fallback mechanisms
|
||||
|
||||
### Security Upgrades
|
||||
1. **Remove client-side security**: Eliminate CSS-based protection
|
||||
2. **Implement token validation**: Add real-time JWT verification
|
||||
3. **Secure cookie implementation**: Use httpOnly with security flags
|
||||
4. **Add comprehensive error handling**: User-friendly error messages
|
||||
|
||||
This specification provides a complete reference for implementing secure MSAL PKCE authentication with server-side validation and httpOnly cookie session management.
|
||||
|
|
@ -37,3 +37,4 @@ urllib3==2.5.0
|
|||
uvicorn==0.35.0
|
||||
msal==1.26.0
|
||||
itsdangerous==2.2.0
|
||||
cryptography==41.0.8
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
:root {
|
||||
--primary-color: #667eea;
|
||||
--secondary-color: #764ba2;
|
||||
--accent-color: #f093fb;
|
||||
--primary-color: #f3ae3e;
|
||||
--secondary-color: #f3ae3e;
|
||||
--accent-color: #f3ae3e;
|
||||
--text-dark: #2d3748;
|
||||
--text-light: #718096;
|
||||
--bg-light: #f7fafc;
|
||||
|
|
@ -11,8 +11,8 @@
|
|||
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
/* Microsoft Brand Colors */
|
||||
--ms-blue: #0078d4;
|
||||
--ms-blue-dark: #106ebe;
|
||||
--ms-blue: #f3ae3e;
|
||||
--ms-blue-dark: #f3ae3e;
|
||||
--ms-gray: #5e5e5e;
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ body {
|
|||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(243, 174, 62, 0.1);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
|
|
@ -259,14 +259,14 @@ h2 {
|
|||
border-color: var(--ms-blue-dark);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 120, 212, 0.3);
|
||||
box-shadow: 0 4px 8px rgba(243, 174, 62, 0.3);
|
||||
}
|
||||
|
||||
.btn-microsoft:focus {
|
||||
background-color: var(--ms-blue-dark);
|
||||
border-color: var(--ms-blue-dark);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.2);
|
||||
box-shadow: 0 0 0 3px rgba(243, 174, 62, 0.2);
|
||||
}
|
||||
|
||||
.btn-microsoft:active {
|
||||
|
|
|
|||
|
|
@ -224,9 +224,111 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Agent Modal -->
|
||||
<div class="modal fade" id="editAgentModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Agent</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editAgentForm">
|
||||
<input type="hidden" id="editAgentId">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentName" class="form-label">Agent Name *</label>
|
||||
<input type="text" class="form-control" id="editAgentName" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentTool" class="form-label">Tool *</label>
|
||||
<input type="text" class="form-control" id="editAgentTool" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentStatus" class="form-label">Status</label>
|
||||
<select class="form-select" id="editAgentStatus">
|
||||
<option value="Development">Development</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
<option value="Deprecated">Deprecated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentVersion" class="form-label">Version</label>
|
||||
<input type="text" class="form-control" id="editAgentVersion">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editAgentDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="editAgentDescription" rows="3" maxlength="300"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editAgentPurpose" class="form-label">Purpose</label>
|
||||
<input type="text" class="form-control" id="editAgentPurpose" maxlength="200">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentLocation" class="form-label">Location</label>
|
||||
<input type="text" class="form-control" id="editAgentLocation">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentDepartment" class="form-label">Department</label>
|
||||
<input type="text" class="form-control" id="editAgentDepartment">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editAgentContact" class="form-label">Contact Person</label>
|
||||
<input type="text" class="form-control" id="editAgentContact">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentTags" class="form-label">Tags</label>
|
||||
<input type="text" class="form-control" id="editAgentTags" placeholder="Separate with commas">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentUserbase" class="form-label">Target Userbase</label>
|
||||
<input type="text" class="form-control" id="editAgentUserbase" placeholder="Separate with commas">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editAgentCapabilities" class="form-label">Capabilities</label>
|
||||
<input type="text" class="form-control" id="editAgentCapabilities" placeholder="Separate with commas">
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="editQualityAuditSection">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus">
|
||||
<label class="form-check-label" for="editQualityAuditStatus">
|
||||
<i class="fas fa-certificate me-2"></i>Quality Audit
|
||||
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text" id="editQualityAuditNote">
|
||||
Check this box if the agent has passed quality audit review.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" form="editAgentForm" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
color: white;
|
||||
|
|
@ -237,15 +339,15 @@
|
|||
}
|
||||
|
||||
.admin-card {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
|
|
@ -270,7 +372,7 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -287,7 +389,7 @@
|
|||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(102, 126, 234, 0.05);
|
||||
background-color: rgba(243, 174, 62, 0.05);
|
||||
}
|
||||
|
||||
.card {
|
||||
|
|
@ -297,7 +399,7 @@
|
|||
}
|
||||
|
||||
.card-header {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
background: rgba(243, 174, 62, 0.05);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
border-radius: 16px 16px 0 0 !important;
|
||||
}
|
||||
|
|
@ -310,8 +412,8 @@
|
|||
|
||||
.nav-tabs .nav-link.active {
|
||||
background-color: transparent;
|
||||
border-bottom: 2px solid #667eea;
|
||||
color: #667eea;
|
||||
border-bottom: 2px solid #f3ae3e;
|
||||
color: #f3ae3e;
|
||||
}
|
||||
|
||||
.status-Active { background-color: #d4edda; color: #155724; }
|
||||
|
|
@ -351,6 +453,7 @@ function setupEventListeners() {
|
|||
document.getElementById('agentSearch').addEventListener('input', filterAgents);
|
||||
document.getElementById('agentStatusFilter').addEventListener('change', filterAgents);
|
||||
document.getElementById('editUserForm').addEventListener('submit', handleEditUserSubmit);
|
||||
document.getElementById('editAgentForm').addEventListener('submit', handleEditAgentSubmit);
|
||||
}
|
||||
|
||||
async function loadAdminData() {
|
||||
|
|
@ -467,6 +570,10 @@ function displayAgents(agents) {
|
|||
|
||||
const agentsHtml = agents.map(agent => {
|
||||
const owner = allUsers.find(u => u.email === agent.created_by);
|
||||
const ownerName = owner?.full_name || agent.agent_contact_person || 'Unknown';
|
||||
const ownerEmail = owner?.email || agent.created_by;
|
||||
const avatarText = (owner?.full_name || agent.agent_contact_person || 'U')[0].toUpperCase();
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
|
|
@ -476,11 +583,11 @@ function displayAgents(agents) {
|
|||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="user-avatar-sm me-2">
|
||||
${(owner?.full_name || owner?.email || 'U')[0].toUpperCase()}
|
||||
${avatarText}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-medium">${owner?.full_name || 'Unknown'}</div>
|
||||
<small class="text-muted">${owner?.email || agent.created_by}</small>
|
||||
<div class="fw-medium">${ownerName}</div>
|
||||
<small class="text-muted">${ownerEmail}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -498,6 +605,9 @@ function displayAgents(agents) {
|
|||
<button class="btn btn-outline-primary btn-sm me-1" onclick="viewAgentDetails('${agent.agent_id}')">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm me-1" onclick="editAgentAdmin('${agent.agent_id}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteAgentAdmin('${agent.agent_id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
|
@ -539,11 +649,42 @@ function viewUserDetails(email) {
|
|||
alert(`User Details:\nName: ${user.full_name || 'No Name'}\nEmail: ${user.email}\nType: ${user.is_admin ? 'Admin' : 'User'}\nStatus: ${user.is_active ? 'Active' : 'Inactive'}\nAgents Created: ${userAgents.length}`);
|
||||
}
|
||||
|
||||
function viewAgentDetails(agentId) {
|
||||
const agent = allAgents.find(a => a.agent_id === agentId);
|
||||
if (!agent) return;
|
||||
|
||||
alert(`Agent Details:\nName: ${agent.agent_name}\nStatus: ${agent.agent_status || 'Development'}\nVersion: ${agent.agent_version || 'N/A'}\nDescription: ${agent.agent_description || 'No description'}\nOwner: ${agent.created_by}`);
|
||||
async function viewAgentDetails(agentId) {
|
||||
try {
|
||||
console.log('DEBUG: Admin fetching fresh agent details for ID:', agentId);
|
||||
|
||||
// Fetch fresh agent data from server
|
||||
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '{{ base_path }}/login';
|
||||
return;
|
||||
} else if (response.status === 403) {
|
||||
showError('Not authorized to view this agent');
|
||||
return;
|
||||
} else {
|
||||
showError('Failed to load agent details');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await response.json();
|
||||
console.log('DEBUG: Admin fresh agent data loaded:', agent);
|
||||
|
||||
const qualityAuditText = agent.quality_audit_status ? 'Audited' : 'Not Audited';
|
||||
const auditTrail = agent.quality_audit_updated_by_name && agent.quality_audit_updated_at ?
|
||||
`\nQuality Audit: ${qualityAuditText} by ${agent.quality_audit_updated_by_name} on ${formatDate(agent.quality_audit_updated_at)}` :
|
||||
`\nQuality Audit: ${qualityAuditText}`;
|
||||
|
||||
alert(`Agent Details:\nName: ${agent.agent_name}\nStatus: ${agent.agent_status || 'Development'}\nVersion: ${agent.agent_version || 'N/A'}\nDescription: ${agent.agent_description || 'No description'}\nOwner: ${agent.created_by}${auditTrail}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading agent details:', error);
|
||||
showError('Failed to load agent details. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
function editUser(email) {
|
||||
|
|
@ -675,6 +816,117 @@ async function handleEditUserSubmit(e) {
|
|||
}
|
||||
}
|
||||
|
||||
async function editAgentAdmin(agentId) {
|
||||
try {
|
||||
console.log('DEBUG: Admin fetching fresh agent data for editing:', agentId);
|
||||
|
||||
// Fetch fresh agent data from server
|
||||
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '{{ base_path }}/login';
|
||||
return;
|
||||
} else if (response.status === 403) {
|
||||
showError('Not authorized to edit this agent');
|
||||
return;
|
||||
} else {
|
||||
showError('Failed to load agent for editing');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await response.json();
|
||||
console.log('DEBUG: Admin fresh agent data for editing:', agent);
|
||||
|
||||
// Populate edit form
|
||||
document.getElementById('editAgentId').value = agent.agent_id;
|
||||
document.getElementById('editAgentName').value = agent.agent_name;
|
||||
document.getElementById('editAgentTool').value = agent.agent_tool || '';
|
||||
document.getElementById('editAgentStatus').value = agent.agent_status || 'Development';
|
||||
document.getElementById('editAgentDescription').value = agent.agent_description || '';
|
||||
document.getElementById('editAgentPurpose').value = agent.agent_purpose || '';
|
||||
document.getElementById('editAgentVersion').value = agent.agent_version || '';
|
||||
document.getElementById('editAgentLocation').value = agent.agent_location || '';
|
||||
document.getElementById('editAgentDepartment').value = agent.agent_department || '';
|
||||
document.getElementById('editAgentContact').value = agent.agent_contact_person || '';
|
||||
document.getElementById('editAgentTags').value = agent.agent_tags ? agent.agent_tags.join(', ') : '';
|
||||
document.getElementById('editAgentUserbase').value = agent.agent_userbase ? agent.agent_userbase.join(', ') : '';
|
||||
document.getElementById('editAgentCapabilities').value = agent.agent_capabilities ? agent.agent_capabilities.join(', ') : '';
|
||||
|
||||
// Handle Quality Audit field (admin always has access)
|
||||
document.getElementById('editQualityAuditStatus').checked = agent.quality_audit_status || false;
|
||||
|
||||
// Update the note with audit trail information
|
||||
let noteHtml = 'Check this box if the agent has passed quality audit review.';
|
||||
if (agent.quality_audit_updated_by_name && agent.quality_audit_updated_at) {
|
||||
const action = agent.quality_audit_status ? 'checked' : 'unchecked';
|
||||
const date = formatDate(agent.quality_audit_updated_at);
|
||||
noteHtml += `<br><br><small class="text-info"><i class="fas fa-history me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong> ${action} Quality Audit on ${date}</small>`;
|
||||
} else {
|
||||
noteHtml += '<br><br><small class="text-muted"><i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.</small>';
|
||||
}
|
||||
document.getElementById('editQualityAuditNote').innerHTML = noteHtml;
|
||||
|
||||
// Show edit modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('editAgentModal'));
|
||||
modal.show();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading agent for editing:', error);
|
||||
showError('Failed to load agent for editing. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditAgentSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const agentId = document.getElementById('editAgentId').value;
|
||||
const agentData = {
|
||||
agent_name: document.getElementById('editAgentName').value,
|
||||
agent_tool: document.getElementById('editAgentTool').value,
|
||||
agent_status: document.getElementById('editAgentStatus').value,
|
||||
agent_description: document.getElementById('editAgentDescription').value,
|
||||
agent_purpose: document.getElementById('editAgentPurpose').value,
|
||||
agent_version: document.getElementById('editAgentVersion').value,
|
||||
agent_location: document.getElementById('editAgentLocation').value,
|
||||
agent_department: document.getElementById('editAgentDepartment').value,
|
||||
agent_contact_person: document.getElementById('editAgentContact').value,
|
||||
agent_tags: document.getElementById('editAgentTags').value.split(',').map(s => s.trim()).filter(s => s),
|
||||
agent_userbase: document.getElementById('editAgentUserbase').value.split(',').map(s => s.trim()).filter(s => s),
|
||||
agent_capabilities: document.getElementById('editAgentCapabilities').value.split(',').map(s => s.trim()).filter(s => s),
|
||||
quality_audit_status: document.getElementById('editQualityAuditStatus').checked
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(agentData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Hide the modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('editAgentModal'));
|
||||
modal.hide();
|
||||
|
||||
// Reload data and show success
|
||||
await loadAdminData();
|
||||
showSuccess('Agent updated successfully');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showError(error.detail || 'Failed to update agent');
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Failed to update agent');
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = '{{ base_path }}/';
|
||||
|
|
|
|||
|
|
@ -239,6 +239,13 @@
|
|||
<label for="editAgentName" class="form-label">Agent Name *</label>
|
||||
<input type="text" class="form-control" id="editAgentName" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentTool" class="form-label">Tool *</label>
|
||||
<input type="text" class="form-control" id="editAgentTool" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentStatus" class="form-label">Status</label>
|
||||
<select class="form-select" id="editAgentStatus">
|
||||
|
|
@ -248,6 +255,10 @@
|
|||
<option value="Deprecated">Deprecated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentVersion" class="form-label">Version</label>
|
||||
<input type="text" class="form-control" id="editAgentVersion">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
|
@ -255,15 +266,9 @@
|
|||
<textarea class="form-control" id="editAgentDescription" rows="3" maxlength="300"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentPurpose" class="form-label">Purpose</label>
|
||||
<input type="text" class="form-control" id="editAgentPurpose" maxlength="200">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editAgentVersion" class="form-label">Version</label>
|
||||
<input type="text" class="form-control" id="editAgentVersion">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editAgentPurpose" class="form-label">Purpose</label>
|
||||
<input type="text" class="form-control" id="editAgentPurpose" maxlength="200">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
|
@ -297,6 +302,19 @@
|
|||
<label for="editAgentCapabilities" class="form-label">Capabilities</label>
|
||||
<input type="text" class="form-control" id="editAgentCapabilities" placeholder="Separate with commas">
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="editQualityAuditSection">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus">
|
||||
<label class="form-check-label" for="editQualityAuditStatus">
|
||||
<i class="fas fa-certificate me-2"></i>Quality Audit
|
||||
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text" id="editQualityAuditNote">
|
||||
Only administrators can modify quality audit status.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
@ -337,7 +355,7 @@
|
|||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -349,7 +367,7 @@
|
|||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -375,6 +393,7 @@ let myAgents = [];
|
|||
let currentAgentId = null;
|
||||
let currentView = '{{ current_view }}';
|
||||
let currentUserId = '{{ current_user._id|string }}';
|
||||
let currentUserIsAdmin = {{ 'true' if current_user.is_admin else 'false' }};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load both datasets initially for accurate counts
|
||||
|
|
@ -420,12 +439,19 @@ async function loadAgentsForCurrentView() {
|
|||
|
||||
async function loadAllAgents() {
|
||||
try {
|
||||
console.log('DEBUG: Loading all agents from server...');
|
||||
const response = await fetch('{{ base_path }}/api/agents/all', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
allAgents = await response.json();
|
||||
console.log('DEBUG: Loaded all agents:', allAgents.length, 'agents');
|
||||
console.log('DEBUG: First agent Quality Audit data:', allAgents.length > 0 ? {
|
||||
name: allAgents[0].agent_name,
|
||||
quality_audit_status: allAgents[0].quality_audit_status,
|
||||
quality_audit_updated_by_name: allAgents[0].quality_audit_updated_by_name
|
||||
} : 'No agents');
|
||||
agents = allAgents;
|
||||
displayAgents(agents);
|
||||
updateAgentCounts();
|
||||
|
|
@ -535,6 +561,7 @@ function displayAgents(agentsToShow) {
|
|||
${agent.agent_status || 'Development'}
|
||||
</span>
|
||||
${agent.agent_version ? `<span class="badge bg-light text-dark">v${agent.agent_version}</span>` : ''}
|
||||
${agent.quality_audit_status ? '<span class="badge bg-success" title="Quality Audited"><i class="fas fa-certificate"></i></span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
|
|
@ -566,12 +593,38 @@ function displayAgents(agentsToShow) {
|
|||
container.innerHTML = agentsHtml;
|
||||
}
|
||||
|
||||
function showAgentDetails(agentId) {
|
||||
const agent = agents.find(a => a.agent_id === agentId);
|
||||
if (!agent) return;
|
||||
|
||||
async function showAgentDetails(agentId) {
|
||||
console.log('DEBUG: Fetching fresh agent details for ID:', agentId);
|
||||
currentAgentId = agentId;
|
||||
|
||||
try {
|
||||
// Fetch fresh agent data from server
|
||||
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '{{ base_path }}/login';
|
||||
return;
|
||||
} else if (response.status === 403) {
|
||||
showError('Not authorized to view this agent');
|
||||
return;
|
||||
} else {
|
||||
showError('Failed to load agent details');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await response.json();
|
||||
console.log('DEBUG: Fresh agent data loaded:', agent);
|
||||
console.log('DEBUG: Quality Audit Details:', {
|
||||
status: agent.quality_audit_status,
|
||||
updated_by: agent.quality_audit_updated_by,
|
||||
updated_by_name: agent.quality_audit_updated_by_name,
|
||||
updated_at: agent.quality_audit_updated_at
|
||||
});
|
||||
|
||||
const modalContent = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
|
@ -581,6 +634,12 @@ function showAgentDetails(agentId) {
|
|||
<tr><td><strong>Status:</strong></td><td><span class="agent-status status-${agent.agent_status || 'Development'}">${agent.agent_status || 'Development'}</span></td></tr>
|
||||
<tr><td><strong>Version:</strong></td><td>${agent.agent_version || 'N/A'}</td></tr>
|
||||
<tr><td><strong>Purpose:</strong></td><td>${agent.agent_purpose || 'N/A'}</td></tr>
|
||||
<tr><td><strong>Quality Audit:</strong></td><td>
|
||||
${agent.quality_audit_status ?
|
||||
'<span class="badge bg-success"><i class="fas fa-certificate me-1"></i>Audited</span>' :
|
||||
'<span class="badge bg-secondary">Not Audited</span>'
|
||||
}
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
|
@ -636,6 +695,23 @@ function showAgentDetails(agentId) {
|
|||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>Quality Audit History</h6>
|
||||
${agent.quality_audit_updated_by_name && agent.quality_audit_updated_at ? `
|
||||
<p class="text-muted small mb-0">
|
||||
<i class="fas fa-user me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong>
|
||||
${agent.quality_audit_status ? 'checked' : 'unchecked'} Quality Audit on
|
||||
<strong>${formatDate(agent.quality_audit_updated_at)}</strong>
|
||||
</p>
|
||||
` : `
|
||||
<p class="text-muted small mb-0">
|
||||
<i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.
|
||||
</p>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h6>Usage Analytics</h6>
|
||||
|
|
@ -693,20 +769,51 @@ function showAgentDetails(agentId) {
|
|||
document.getElementById('deleteAgentBtn').addEventListener('click', deleteAgent);
|
||||
}
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('agentModal'));
|
||||
modal.show();
|
||||
|
||||
// Load usage data after modal is shown
|
||||
loadUsageChart(agent.agent_name);
|
||||
const modal = new bootstrap.Modal(document.getElementById('agentModal'));
|
||||
modal.show();
|
||||
|
||||
// Load usage data after modal is shown
|
||||
loadUsageChart(agent.agent_name);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading agent details:', error);
|
||||
showError('Failed to load agent details. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
function showEditModal() {
|
||||
const agent = agents.find(a => a.agent_id === currentAgentId);
|
||||
if (!agent) return;
|
||||
async function showEditModal() {
|
||||
console.log('showEditModal called with currentAgentId:', currentAgentId);
|
||||
|
||||
try {
|
||||
// Fetch fresh agent data from server
|
||||
const response = await fetch(`{{ base_path }}/api/agents/${currentAgentId}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '{{ base_path }}/login';
|
||||
return;
|
||||
} else if (response.status === 403) {
|
||||
showError('Not authorized to edit this agent');
|
||||
return;
|
||||
} else {
|
||||
showError('Failed to load agent for editing');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await response.json();
|
||||
console.log('DEBUG: Fresh agent data for editing:', agent);
|
||||
console.log('Agent Quality Audit Status:', agent.quality_audit_status);
|
||||
console.log('Agent Quality Audit Updated By:', agent.quality_audit_updated_by_name);
|
||||
console.log('Agent Quality Audit Updated At:', agent.quality_audit_updated_at);
|
||||
console.log('Agent Quality Audit Updated By ID:', agent.quality_audit_updated_by);
|
||||
|
||||
// Populate edit form
|
||||
document.getElementById('editAgentId').value = agent.agent_id;
|
||||
document.getElementById('editAgentName').value = agent.agent_name;
|
||||
document.getElementById('editAgentTool').value = agent.agent_tool || '';
|
||||
document.getElementById('editAgentStatus').value = agent.agent_status || 'Development';
|
||||
document.getElementById('editAgentDescription').value = agent.agent_description || '';
|
||||
document.getElementById('editAgentPurpose').value = agent.agent_purpose || '';
|
||||
|
|
@ -718,10 +825,44 @@ function showEditModal() {
|
|||
document.getElementById('editAgentUserbase').value = agent.agent_userbase ? agent.agent_userbase.join(', ') : '';
|
||||
document.getElementById('editAgentCapabilities').value = agent.agent_capabilities ? agent.agent_capabilities.join(', ') : '';
|
||||
|
||||
// Hide details modal and show edit modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('agentModal')).hide();
|
||||
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
editModal.show();
|
||||
// Handle Quality Audit field
|
||||
const qualityAuditCheckbox = document.getElementById('editQualityAuditStatus');
|
||||
const qualityAuditSection = document.getElementById('editQualityAuditSection');
|
||||
|
||||
if (currentUserIsAdmin) {
|
||||
qualityAuditSection.style.display = 'block';
|
||||
qualityAuditCheckbox.checked = agent.quality_audit_status || false;
|
||||
qualityAuditCheckbox.disabled = false;
|
||||
|
||||
// Update the note with audit trail information
|
||||
let noteHtml = 'Check this box if the agent has passed quality audit review.';
|
||||
if (agent.quality_audit_updated_by_name && agent.quality_audit_updated_at) {
|
||||
const action = agent.quality_audit_status ? 'checked' : 'unchecked';
|
||||
const date = formatDate(agent.quality_audit_updated_at);
|
||||
noteHtml += `<br><br><small class="text-info"><i class="fas fa-history me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong> ${action} Quality Audit on ${date}</small>`;
|
||||
} else {
|
||||
noteHtml += '<br><br><small class="text-muted"><i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.</small>';
|
||||
}
|
||||
document.getElementById('editQualityAuditNote').innerHTML = noteHtml;
|
||||
} else {
|
||||
qualityAuditSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Hide details modal if it's open, then show edit modal
|
||||
const agentModal = bootstrap.Modal.getInstance(document.getElementById('agentModal'));
|
||||
if (agentModal) {
|
||||
agentModal.hide();
|
||||
}
|
||||
|
||||
console.log('About to show edit modal');
|
||||
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
editModal.show();
|
||||
console.log('Edit modal show() called');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading agent for editing:', error);
|
||||
showError('Failed to load agent for editing. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAgent(e) {
|
||||
|
|
@ -730,6 +871,7 @@ async function updateAgent(e) {
|
|||
const agentId = document.getElementById('editAgentId').value;
|
||||
const agentData = {
|
||||
agent_name: document.getElementById('editAgentName').value,
|
||||
agent_tool: document.getElementById('editAgentTool').value,
|
||||
agent_status: document.getElementById('editAgentStatus').value,
|
||||
agent_description: document.getElementById('editAgentDescription').value,
|
||||
agent_purpose: document.getElementById('editAgentPurpose').value,
|
||||
|
|
@ -742,6 +884,13 @@ async function updateAgent(e) {
|
|||
agent_capabilities: document.getElementById('editAgentCapabilities').value.split(',').map(s => s.trim()).filter(s => s)
|
||||
};
|
||||
|
||||
// Add Quality Audit status if user is admin
|
||||
if (currentUserIsAdmin) {
|
||||
agentData.quality_audit_status = document.getElementById('editQualityAuditStatus').checked;
|
||||
}
|
||||
|
||||
console.log('Sending agent update data:', agentData);
|
||||
|
||||
try {
|
||||
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -753,12 +902,19 @@ async function updateAgent(e) {
|
|||
});
|
||||
|
||||
if (response.ok) {
|
||||
const updatedAgent = await response.json();
|
||||
console.log('Update successful, received agent data:', updatedAgent);
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
await loadAgentsForCurrentView();
|
||||
|
||||
// Properly reload data from server instead of using cached data
|
||||
await Promise.all([loadAllAgents(), loadMyAgents()]);
|
||||
loadAgentsForCurrentView();
|
||||
|
||||
showSuccess('Agent updated successfully');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showError(error.detail || 'Failed to update agent');
|
||||
console.error('Update failed with response:', response.status, error);
|
||||
showError(error.detail || `Failed to update agent (${response.status})`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Failed to update agent');
|
||||
|
|
@ -855,6 +1011,8 @@ function showError(message) {
|
|||
}
|
||||
|
||||
function editAgent(agentId) {
|
||||
console.log('Edit button clicked for agent:', agentId);
|
||||
console.log('Current agents array:', agents);
|
||||
currentAgentId = agentId;
|
||||
showEditModal();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,19 @@
|
|||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="agentTool" class="form-label">
|
||||
<i class="fas fa-tools me-2"></i>Tool *
|
||||
</label>
|
||||
<input type="text"
|
||||
name="agent_tool"
|
||||
class="form-control form-control-lg"
|
||||
id="agentTool"
|
||||
placeholder="e.g., chat-sandbox (LibreChat), Copilot, Custom"
|
||||
required>
|
||||
<div class="form-text">The platform or environment where this agent operates</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="agentDescription" class="form-label">
|
||||
<i class="fas fa-align-left me-2"></i>Description
|
||||
|
|
@ -155,6 +168,32 @@
|
|||
<div class="form-text">Separate multiple capabilities with commas</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="quality_audit_status"
|
||||
value="true"
|
||||
id="qualityAuditStatus"
|
||||
{% if not current_user.is_admin %}disabled{% endif %}>
|
||||
<label class="form-check-label" for="qualityAuditStatus">
|
||||
<i class="fas fa-certificate me-2"></i>Quality Audit
|
||||
{% if current_user.is_admin %}
|
||||
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary ms-2">Admin Required</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text" id="qualityAuditNote">
|
||||
{% if current_user.is_admin %}
|
||||
Check this box if the agent has passed quality audit review.
|
||||
{% else %}
|
||||
Only administrators can mark agents as quality audited.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid mb-4">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-robot me-2"></i>Register Agent
|
||||
|
|
@ -180,7 +219,7 @@
|
|||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -223,6 +262,7 @@ let formSubmitted = false;
|
|||
|
||||
document.getElementById('agentForm').addEventListener('submit', function(e) {
|
||||
const agentName = document.getElementById('agentName').value.trim();
|
||||
const agentTool = document.getElementById('agentTool').value.trim();
|
||||
|
||||
if (!agentName) {
|
||||
e.preventDefault();
|
||||
|
|
@ -230,6 +270,12 @@ document.getElementById('agentForm').addEventListener('submit', function(e) {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!agentTool) {
|
||||
e.preventDefault();
|
||||
alert('Agent tool is required!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent duplicate submissions
|
||||
if (formSubmitted) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -19,20 +19,23 @@
|
|||
{% if msal_enabled %}
|
||||
<!-- Microsoft Sign-in Button -->
|
||||
<div class="d-grid mb-4">
|
||||
<a href="{{ base_path }}/auth/azure/login" class="btn btn-microsoft btn-lg">
|
||||
<button type="button" id="microsoftLoginBtn" class="btn btn-microsoft btn-lg">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" class="me-2">
|
||||
<path fill="currentColor" d="M11.4 24H0V12.6h11.4V24zM24 24H12.6V12.6H24V24zM11.4 11.4H0V0h11.4v11.4zM24 11.4H12.6V0H24v11.4z"/>
|
||||
</svg>
|
||||
Sign in with Microsoft
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if show_local_login %}
|
||||
<!-- Divider -->
|
||||
<div class="auth-divider mb-4">
|
||||
<span class="auth-divider-text">or continue with email</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_local_login %}
|
||||
<!-- Local Authentication Section -->
|
||||
<div class="local-auth-section">
|
||||
<p class="text-muted small mb-3 text-center">
|
||||
|
|
@ -86,18 +89,13 @@
|
|||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-muted mb-2">
|
||||
Don't have an account?
|
||||
<a href="{{ base_path }}/register" class="text-decoration-none fw-bold">
|
||||
Create one here
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-muted small">
|
||||
<em>Local login is for administrators and development use only</em>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- End local-auth-section -->
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -173,6 +171,11 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
{% if msal_enabled %}
|
||||
<!-- MSAL Browser Library -->
|
||||
<script src="https://alcdn.msauth.net/browser/2.38.1/js/msal-browser.min.js"></script>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function togglePassword(inputId) {
|
||||
const passwordInput = document.getElementById(inputId);
|
||||
|
|
@ -186,5 +189,70 @@ function togglePassword(inputId) {
|
|||
icon.className = 'fas fa-eye';
|
||||
}
|
||||
}
|
||||
|
||||
{% if msal_enabled %}
|
||||
// MSAL configuration following specification
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: "{{ client_id }}",
|
||||
authority: "{{ authority }}",
|
||||
redirectUri: "{{ redirect_uri }}"
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "sessionStorage",
|
||||
storeAuthStateInCookie: true,
|
||||
}
|
||||
};
|
||||
|
||||
const loginRequest = {
|
||||
scopes: ["openid", "profile", "email"],
|
||||
prompt: "select_account"
|
||||
};
|
||||
|
||||
// Initialize MSAL instance
|
||||
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
// Microsoft login handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const microsoftBtn = document.getElementById('microsoftLoginBtn');
|
||||
if (microsoftBtn) {
|
||||
microsoftBtn.addEventListener('click', async function() {
|
||||
try {
|
||||
// Use popup-based login as per specification
|
||||
const response = await myMSALObj.loginPopup(loginRequest);
|
||||
|
||||
// Send tokens to server for validation and session creation
|
||||
const serverResponse = await fetch('{{ base_path }}/api/auth/azure/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
access_token: response.accessToken,
|
||||
id_token: response.idToken
|
||||
})
|
||||
});
|
||||
|
||||
if (!serverResponse.ok) {
|
||||
throw new Error('Failed to authenticate with server');
|
||||
}
|
||||
|
||||
const result = await serverResponse.json();
|
||||
|
||||
// Redirect based on user role
|
||||
if (result.is_admin) {
|
||||
window.location.href = '{{ base_path }}/admin';
|
||||
} else {
|
||||
window.location.href = '{{ base_path }}/agent-management';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
alert('Authentication failed: ' + error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
|
||||
<style>
|
||||
.border-left-primary {
|
||||
border-left: 4px solid #007bff !important;
|
||||
border-left: 4px solid #f3ae3e !important;
|
||||
}
|
||||
|
||||
.badge-status-active {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue