Files
claudetools/api/routers/clients.py
Mike Swanson 390b10b32c Complete Phase 6: MSP Work Tracking with Context Recall System
Implements production-ready MSP platform with cross-machine persistent memory for Claude.

API Implementation:
- 130 REST API endpoints across 21 entities
- JWT authentication on all endpoints
- AES-256-GCM encryption for credentials
- Automatic audit logging
- Complete OpenAPI documentation

Database:
- 43 tables in MariaDB (172.16.3.20:3306)
- 42 SQLAlchemy models with modern 2.0 syntax
- Full Alembic migration system
- 99.1% CRUD test pass rate

Context Recall System (Phase 6):
- Cross-machine persistent memory via database
- Automatic context injection via Claude Code hooks
- Automatic context saving after task completion
- 90-95% token reduction with compression utilities
- Relevance scoring with time decay
- Tag-based semantic search
- One-command setup script

Security Features:
- JWT tokens with Argon2 password hashing
- AES-256-GCM encryption for all sensitive data
- Comprehensive audit trail for credentials
- HMAC tamper detection
- Secure configuration management

Test Results:
- Phase 3: 38/38 CRUD tests passing (100%)
- Phase 4: 34/35 core API tests passing (97.1%)
- Phase 5: 62/62 extended API tests passing (100%)
- Phase 6: 10/10 compression tests passing (100%)
- Overall: 144/145 tests passing (99.3%)

Documentation:
- Comprehensive architecture guides
- Setup automation scripts
- API documentation at /api/docs
- Complete test reports
- Troubleshooting guides

Project Status: 95% Complete (Production-Ready)
Phase 7 (optional work context APIs) remains for future enhancement.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 06:00:26 -07:00

380 lines
10 KiB
Python

"""
Client API router for ClaudeTools.
This module defines all REST API endpoints for managing clients, including
CRUD operations with proper authentication, validation, and error handling.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from api.database import get_db
from api.middleware.auth import get_current_user
from api.schemas.client import (
ClientCreate,
ClientResponse,
ClientUpdate,
)
from api.services import client_service
# Create router with prefix and tags
router = APIRouter()
@router.get(
"",
response_model=dict,
summary="List all clients",
description="Retrieve a paginated list of all clients with optional filtering",
status_code=status.HTTP_200_OK,
)
def list_clients(
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination"
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (max 1000)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
List all clients with pagination.
- **skip**: Number of clients to skip (default: 0)
- **limit**: Maximum number of clients to return (default: 100, max: 1000)
Returns a list of clients with pagination metadata.
**Example Request:**
```
GET /api/clients?skip=0&limit=50
Authorization: Bearer <token>
```
**Example Response:**
```json
{
"total": 5,
"skip": 0,
"limit": 50,
"clients": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Corporation",
"type": "msp_client",
"network_subnet": "192.168.0.0/24",
"domain_name": "acme.local",
"m365_tenant_id": "abc12345-6789-0def-1234-56789abcdef0",
"primary_contact": "John Doe",
"notes": "Main MSP client",
"is_active": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
]
}
```
"""
try:
clients, total = client_service.get_clients(db, skip, limit)
return {
"total": total,
"skip": skip,
"limit": limit,
"clients": [ClientResponse.model_validate(client) for client in clients]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve clients: {str(e)}"
)
@router.get(
"/{client_id}",
response_model=ClientResponse,
summary="Get client by ID",
description="Retrieve a single client by its unique identifier",
status_code=status.HTTP_200_OK,
responses={
200: {
"description": "Client found and returned",
"model": ClientResponse,
},
404: {
"description": "Client not found",
"content": {
"application/json": {
"example": {"detail": "Client with ID 123e4567-e89b-12d3-a456-426614174000 not found"}
}
},
},
},
)
def get_client(
client_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific client by ID.
- **client_id**: UUID of the client to retrieve
Returns the complete client details.
**Example Request:**
```
GET /api/clients/123e4567-e89b-12d3-a456-426614174000
Authorization: Bearer <token>
```
**Example Response:**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Corporation",
"type": "msp_client",
"network_subnet": "192.168.0.0/24",
"domain_name": "acme.local",
"m365_tenant_id": "abc12345-6789-0def-1234-56789abcdef0",
"primary_contact": "John Doe",
"notes": "Main MSP client",
"is_active": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
```
"""
client = client_service.get_client_by_id(db, client_id)
return ClientResponse.model_validate(client)
@router.post(
"",
response_model=ClientResponse,
summary="Create new client",
description="Create a new client with the provided details",
status_code=status.HTTP_201_CREATED,
responses={
201: {
"description": "Client created successfully",
"model": ClientResponse,
},
409: {
"description": "Client with name already exists",
"content": {
"application/json": {
"example": {"detail": "Client with name 'Acme Corporation' already exists"}
}
},
},
422: {
"description": "Validation error",
"content": {
"application/json": {
"example": {
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
}
},
},
},
)
def create_client(
client_data: ClientCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new client.
Requires a valid JWT token with appropriate permissions.
**Example Request:**
```json
POST /api/clients
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Acme Corporation",
"type": "msp_client",
"network_subnet": "192.168.0.0/24",
"domain_name": "acme.local",
"m365_tenant_id": "abc12345-6789-0def-1234-56789abcdef0",
"primary_contact": "John Doe",
"notes": "Main MSP client",
"is_active": true
}
```
**Example Response:**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Corporation",
"type": "msp_client",
"network_subnet": "192.168.0.0/24",
"domain_name": "acme.local",
"m365_tenant_id": "abc12345-6789-0def-1234-56789abcdef0",
"primary_contact": "John Doe",
"notes": "Main MSP client",
"is_active": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
```
"""
client = client_service.create_client(db, client_data)
return ClientResponse.model_validate(client)
@router.put(
"/{client_id}",
response_model=ClientResponse,
summary="Update client",
description="Update an existing client's details",
status_code=status.HTTP_200_OK,
responses={
200: {
"description": "Client updated successfully",
"model": ClientResponse,
},
404: {
"description": "Client not found",
"content": {
"application/json": {
"example": {"detail": "Client with ID 123e4567-e89b-12d3-a456-426614174000 not found"}
}
},
},
409: {
"description": "Conflict with existing client",
"content": {
"application/json": {
"example": {"detail": "Client with name 'Acme Corporation' already exists"}
}
},
},
},
)
def update_client(
client_id: UUID,
client_data: ClientUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing client.
- **client_id**: UUID of the client to update
Only provided fields will be updated. All fields are optional.
**Example Request:**
```json
PUT /api/clients/123e4567-e89b-12d3-a456-426614174000
Authorization: Bearer <token>
Content-Type: application/json
{
"primary_contact": "Jane Smith",
"is_active": false,
"notes": "Client moved to inactive status"
}
```
**Example Response:**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Corporation",
"type": "msp_client",
"network_subnet": "192.168.0.0/24",
"domain_name": "acme.local",
"m365_tenant_id": "abc12345-6789-0def-1234-56789abcdef0",
"primary_contact": "Jane Smith",
"notes": "Client moved to inactive status",
"is_active": false,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T14:20:00Z"
}
```
"""
client = client_service.update_client(db, client_id, client_data)
return ClientResponse.model_validate(client)
@router.delete(
"/{client_id}",
response_model=dict,
summary="Delete client",
description="Delete a client by its ID",
status_code=status.HTTP_200_OK,
responses={
200: {
"description": "Client deleted successfully",
"content": {
"application/json": {
"example": {
"message": "Client deleted successfully",
"client_id": "123e4567-e89b-12d3-a456-426614174000"
}
}
},
},
404: {
"description": "Client not found",
"content": {
"application/json": {
"example": {"detail": "Client with ID 123e4567-e89b-12d3-a456-426614174000 not found"}
}
},
},
},
)
def delete_client(
client_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a client.
- **client_id**: UUID of the client to delete
This is a permanent operation and cannot be undone.
**Example Request:**
```
DELETE /api/clients/123e4567-e89b-12d3-a456-426614174000
Authorization: Bearer <token>
```
**Example Response:**
```json
{
"message": "Client deleted successfully",
"client_id": "123e4567-e89b-12d3-a456-426614174000"
}
```
"""
return client_service.delete_client(db, client_id)