mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 15:34:36 +01:00
simplifying the auth and creating strict separation
This commit is contained in:
@@ -16,7 +16,7 @@ from ..v1.prompt_templates import router as prompt_templates_router
|
||||
from ..v1.security import router as security_router
|
||||
from ..v1.plugin_registry import router as plugin_registry_router
|
||||
from ..v1.platform import router as platform_router
|
||||
from ..v1.llm import router as llm_router
|
||||
from ..v1.llm_internal import router as llm_internal_router
|
||||
from ..v1.chatbot import router as chatbot_router
|
||||
|
||||
# Create internal API router
|
||||
@@ -61,8 +61,8 @@ internal_api_router.include_router(security_router, prefix="/security", tags=["i
|
||||
# Include plugin registry routes (frontend plugin management)
|
||||
internal_api_router.include_router(plugin_registry_router, prefix="/plugins", tags=["internal-plugins"])
|
||||
|
||||
# Include LLM routes (frontend LLM service access)
|
||||
internal_api_router.include_router(llm_router, prefix="/llm", tags=["internal-llm"])
|
||||
# Include internal LLM routes (frontend LLM service access with JWT auth)
|
||||
internal_api_router.include_router(llm_internal_router, prefix="/llm", tags=["internal-llm"])
|
||||
|
||||
# Include chatbot routes (frontend chatbot management)
|
||||
internal_api_router.include_router(chatbot_router, prefix="/chatbot", tags=["internal-chatbot"])
|
||||
@@ -126,62 +126,13 @@ class ModelsResponse(BaseModel):
|
||||
data: List[ModelInfo]
|
||||
|
||||
|
||||
# Hybrid authentication function
|
||||
async def get_auth_context(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get authentication context from either API key or JWT token"""
|
||||
# Try API key authentication first
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
|
||||
# Check if it's an API key (starts with ce_ prefix)
|
||||
if token.startswith(settings.API_KEY_PREFIX):
|
||||
try:
|
||||
context = await get_api_key_context(request, db)
|
||||
if context:
|
||||
return context
|
||||
except Exception as e:
|
||||
logger.warning(f"API key authentication failed: {e}")
|
||||
else:
|
||||
# Try JWT token authentication
|
||||
try:
|
||||
from app.core.security import get_current_user
|
||||
# Create a fake credentials object for JWT validation
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
|
||||
user = await get_current_user(credentials, db)
|
||||
if user:
|
||||
return {
|
||||
"user": user,
|
||||
"auth_type": "jwt",
|
||||
"api_key": None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"JWT authentication failed: {e}")
|
||||
|
||||
# Try X-API-Key header
|
||||
api_key = request.headers.get("X-API-Key")
|
||||
if api_key:
|
||||
try:
|
||||
context = await get_api_key_context(request, db)
|
||||
if context:
|
||||
return context
|
||||
except Exception as e:
|
||||
logger.warning(f"X-API-Key authentication failed: {e}")
|
||||
|
||||
# No valid authentication found
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Valid API key or authentication token required"
|
||||
)
|
||||
# Authentication: Public API endpoints should use require_api_key
|
||||
# Internal API endpoints should use get_current_user from core.security
|
||||
|
||||
# Endpoints
|
||||
@router.get("/models", response_model=ModelsResponse)
|
||||
async def list_models(
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List available models"""
|
||||
@@ -220,7 +171,7 @@ async def list_models(
|
||||
|
||||
@router.post("/models/invalidate-cache")
|
||||
async def invalidate_models_cache_endpoint(
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Invalidate models cache (admin only)"""
|
||||
@@ -249,7 +200,7 @@ async def invalidate_models_cache_endpoint(
|
||||
async def create_chat_completion(
|
||||
request_body: Request,
|
||||
chat_request: ChatCompletionRequest,
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create chat completion with budget enforcement"""
|
||||
@@ -604,7 +555,7 @@ async def create_embedding(
|
||||
|
||||
@router.get("/health")
|
||||
async def llm_health_check(
|
||||
context: Dict[str, Any] = Depends(get_auth_context)
|
||||
context: Dict[str, Any] = Depends(require_api_key)
|
||||
):
|
||||
"""Health check for LLM service"""
|
||||
try:
|
||||
@@ -686,7 +637,7 @@ async def get_usage_stats(
|
||||
@router.get("/budget/status")
|
||||
async def get_budget_status(
|
||||
request: Request,
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get current budget status and usage analytics"""
|
||||
|
||||
111
backend/app/api/v1/llm_internal.py
Normal file
111
backend/app/api/v1/llm_internal.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Internal LLM API endpoints - for frontend use with JWT authentication
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.services.llm.service import llm_service
|
||||
from app.api.v1.llm import get_cached_models # Reuse the caching logic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/models")
|
||||
async def list_models(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
List available LLM models for authenticated users
|
||||
"""
|
||||
try:
|
||||
models = await get_cached_models()
|
||||
return {"data": models}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list models: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve models"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/providers/status")
|
||||
async def get_provider_status(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get status of all LLM providers for authenticated users
|
||||
"""
|
||||
try:
|
||||
provider_status = await llm_service.get_provider_status()
|
||||
return {
|
||||
"object": "provider_status",
|
||||
"data": {
|
||||
name: {
|
||||
"provider": status.provider,
|
||||
"status": status.status,
|
||||
"latency_ms": status.latency_ms,
|
||||
"success_rate": status.success_rate,
|
||||
"last_check": status.last_check.isoformat(),
|
||||
"error_message": status.error_message,
|
||||
"models_available": status.models_available
|
||||
}
|
||||
for name, status in provider_status.items()
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get provider status: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve provider status"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get LLM service health status for authenticated users
|
||||
"""
|
||||
try:
|
||||
health = await llm_service.health_check()
|
||||
return {
|
||||
"status": health["status"],
|
||||
"providers": health.get("providers", {}),
|
||||
"timestamp": health.get("timestamp")
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Health check failed"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics")
|
||||
async def get_metrics(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get LLM service metrics for authenticated users
|
||||
"""
|
||||
try:
|
||||
metrics = await llm_service.get_metrics()
|
||||
return {
|
||||
"object": "metrics",
|
||||
"data": metrics
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get metrics: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve metrics"
|
||||
)
|
||||
@@ -6,12 +6,13 @@ from typing import Dict, Any, List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from app.services.module_manager import module_manager, ModuleConfig
|
||||
from app.core.logging import log_api_request
|
||||
from app.core.security import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_modules():
|
||||
async def list_modules(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Get list of all discovered modules with their status (enabled and disabled)"""
|
||||
log_api_request("list_modules", {})
|
||||
|
||||
@@ -70,7 +71,7 @@ async def list_modules():
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_modules_status():
|
||||
async def get_modules_status(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Get comprehensive module status - CONSOLIDATED endpoint"""
|
||||
log_api_request("get_modules_status", {})
|
||||
|
||||
@@ -134,7 +135,7 @@ async def get_modules_status():
|
||||
|
||||
|
||||
@router.get("/{module_name}")
|
||||
async def get_module_info(module_name: str):
|
||||
async def get_module_info(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Get detailed information about a specific module"""
|
||||
log_api_request("get_module_info", {"module_name": module_name})
|
||||
|
||||
@@ -187,7 +188,7 @@ async def get_module_info(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/enable")
|
||||
async def enable_module(module_name: str):
|
||||
async def enable_module(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Enable a module"""
|
||||
log_api_request("enable_module", {"module_name": module_name})
|
||||
|
||||
@@ -212,7 +213,7 @@ async def enable_module(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/disable")
|
||||
async def disable_module(module_name: str):
|
||||
async def disable_module(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Disable a module"""
|
||||
log_api_request("disable_module", {"module_name": module_name})
|
||||
|
||||
@@ -237,7 +238,7 @@ async def disable_module(module_name: str):
|
||||
|
||||
|
||||
@router.post("/all/reload")
|
||||
async def reload_all_modules():
|
||||
async def reload_all_modules(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Reload all modules"""
|
||||
log_api_request("reload_all_modules", {})
|
||||
|
||||
@@ -271,7 +272,7 @@ async def reload_all_modules():
|
||||
|
||||
|
||||
@router.post("/{module_name}/reload")
|
||||
async def reload_module(module_name: str):
|
||||
async def reload_module(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Reload a specific module"""
|
||||
log_api_request("reload_module", {"module_name": module_name})
|
||||
|
||||
@@ -290,7 +291,7 @@ async def reload_module(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/restart")
|
||||
async def restart_module(module_name: str):
|
||||
async def restart_module(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Restart a specific module (alias for reload)"""
|
||||
log_api_request("restart_module", {"module_name": module_name})
|
||||
|
||||
@@ -309,7 +310,7 @@ async def restart_module(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/start")
|
||||
async def start_module(module_name: str):
|
||||
async def start_module(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Start a specific module (enable and load)"""
|
||||
log_api_request("start_module", {"module_name": module_name})
|
||||
|
||||
@@ -331,7 +332,7 @@ async def start_module(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/stop")
|
||||
async def stop_module(module_name: str):
|
||||
async def stop_module(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Stop a specific module (disable and unload)"""
|
||||
log_api_request("stop_module", {"module_name": module_name})
|
||||
|
||||
@@ -353,7 +354,7 @@ async def stop_module(module_name: str):
|
||||
|
||||
|
||||
@router.get("/{module_name}/stats")
|
||||
async def get_module_stats(module_name: str):
|
||||
async def get_module_stats(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Get module statistics"""
|
||||
log_api_request("get_module_stats", {"module_name": module_name})
|
||||
|
||||
@@ -380,7 +381,7 @@ async def get_module_stats(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/execute")
|
||||
async def execute_module_action(module_name: str, request_data: Dict[str, Any]):
|
||||
async def execute_module_action(module_name: str, request_data: Dict[str, Any], current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Execute a module action through the interceptor pattern"""
|
||||
log_api_request("execute_module_action", {"module_name": module_name, "action": request_data.get("action")})
|
||||
|
||||
@@ -442,7 +443,7 @@ async def execute_module_action(module_name: str, request_data: Dict[str, Any]):
|
||||
|
||||
|
||||
@router.get("/{module_name}/config")
|
||||
async def get_module_config(module_name: str):
|
||||
async def get_module_config(module_name: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Get module configuration schema and current values"""
|
||||
log_api_request("get_module_config", {"module_name": module_name})
|
||||
|
||||
@@ -493,7 +494,7 @@ async def get_module_config(module_name: str):
|
||||
|
||||
|
||||
@router.post("/{module_name}/config")
|
||||
async def update_module_config(module_name: str, config: dict):
|
||||
async def update_module_config(module_name: str, config: dict, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Update module configuration"""
|
||||
log_api_request("update_module_config", {"module_name": module_name})
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.services.api_key_auth import require_api_key
|
||||
from app.api.v1.llm import (
|
||||
get_auth_context, get_cached_models, ModelsResponse, ModelInfo,
|
||||
get_cached_models, ModelsResponse, ModelInfo,
|
||||
ChatCompletionRequest, EmbeddingRequest, create_chat_completion as llm_chat_completion,
|
||||
create_embedding as llm_create_embedding
|
||||
)
|
||||
@@ -41,7 +42,7 @@ def openai_error_response(message: str, error_type: str = "invalid_request_error
|
||||
|
||||
@router.get("/models", response_model=ModelsResponse)
|
||||
async def list_models(
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
@@ -84,7 +85,7 @@ async def list_models(
|
||||
async def create_chat_completion(
|
||||
request_body: Request,
|
||||
chat_request: ChatCompletionRequest,
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
@@ -100,7 +101,7 @@ async def create_chat_completion(
|
||||
@router.post("/embeddings")
|
||||
async def create_embedding(
|
||||
request: EmbeddingRequest,
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
@@ -116,7 +117,7 @@ async def create_embedding(
|
||||
@router.get("/models/{model_id}")
|
||||
async def retrieve_model(
|
||||
model_id: str,
|
||||
context: Dict[str, Any] = Depends(get_auth_context),
|
||||
context: Dict[str, Any] = Depends(require_api_key),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -9,6 +9,7 @@ from pydantic import BaseModel
|
||||
|
||||
from app.services.permission_manager import permission_registry, Permission, PermissionScope
|
||||
from app.core.logging import get_logger
|
||||
from app.core.security import get_current_user
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -85,7 +86,7 @@ async def get_available_permissions(namespace: Optional[str] = None):
|
||||
resource=perm.resource,
|
||||
action=perm.action,
|
||||
description=perm.description,
|
||||
conditions=perm.conditions
|
||||
conditions=getattr(perm, 'conditions', None)
|
||||
)
|
||||
for perm in perms
|
||||
]
|
||||
@@ -131,7 +132,10 @@ async def validate_permissions(request: PermissionValidationRequest):
|
||||
|
||||
|
||||
@router.post("/permissions/check", response_model=PermissionCheckResponse)
|
||||
async def check_permission(request: PermissionCheckRequest):
|
||||
async def check_permission(
|
||||
request: PermissionCheckRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Check if user has a specific permission"""
|
||||
try:
|
||||
has_permission = permission_registry.check_permission(
|
||||
@@ -168,7 +172,7 @@ async def get_module_permissions(module_id: str):
|
||||
resource=perm.resource,
|
||||
action=perm.action,
|
||||
description=perm.description,
|
||||
conditions=perm.conditions
|
||||
conditions=getattr(perm, 'conditions', None)
|
||||
)
|
||||
for perm in permissions
|
||||
]
|
||||
|
||||
@@ -72,7 +72,9 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
try:
|
||||
yield session
|
||||
except Exception as e:
|
||||
logger.error(f"Database session error: {e}")
|
||||
# Only log if there's an actual error, not normal operation
|
||||
if str(e).strip(): # Only log if error message exists
|
||||
logger.error(f"Database session error: {str(e)}", exc_info=True)
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
|
||||
@@ -104,8 +104,10 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
||||
"/favicon.ico",
|
||||
"/api/v1/auth/register",
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/refresh", # Allow refresh endpoint
|
||||
"/api-internal/v1/auth/register",
|
||||
"/api-internal/v1/auth/login",
|
||||
"/api-internal/v1/auth/refresh", # Allow refresh endpoint for internal API
|
||||
"/", # Root endpoint
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Handles authentication, routing, and security for plugin APIs
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
import jwt
|
||||
from jose import jwt
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Plugin Security and Authentication Service
|
||||
Handles plugin tokens, permissions, and security policies
|
||||
"""
|
||||
import jwt
|
||||
from jose import jwt
|
||||
import hashlib
|
||||
import secrets
|
||||
import time
|
||||
|
||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"events": "^3.3.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.2.32",
|
||||
@@ -3973,6 +3974,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"events": "^3.3.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.2.32",
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useState } from "react"
|
||||
import { apiClient } from "@/lib/api-client"
|
||||
import { decodeToken } from "@/lib/auth-utils"
|
||||
import { tokenManager } from "@/lib/token-manager"
|
||||
|
||||
export default function TestAuthPage() {
|
||||
const { user, token, refreshToken, logout } = useAuth()
|
||||
const { user, logout, isAuthenticated } = useAuth()
|
||||
const [testResult, setTestResult] = useState<string>("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
@@ -24,19 +24,19 @@ export default function TestAuthPage() {
|
||||
}
|
||||
|
||||
const getTokenInfo = () => {
|
||||
if (!token) return "No token"
|
||||
const expiry = tokenManager.getTokenExpiry()
|
||||
const refreshExpiry = tokenManager.getRefreshTokenExpiry()
|
||||
|
||||
const payload = decodeToken(token)
|
||||
if (!payload) return "Invalid token"
|
||||
if (!expiry) return "No token"
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const timeUntilExpiry = payload.exp - now
|
||||
const expiryDate = new Date(payload.exp * 1000)
|
||||
const now = new Date()
|
||||
const timeUntilExpiry = Math.floor((expiry.getTime() - now.getTime()) / 1000)
|
||||
|
||||
return `
|
||||
Token expires in: ${Math.floor(timeUntilExpiry / 60)} minutes ${timeUntilExpiry % 60} seconds
|
||||
Expiry time: ${expiryDate.toLocaleString()}
|
||||
Token payload: ${JSON.stringify(payload, null, 2)}
|
||||
Access token expiry: ${expiry.toLocaleString()}
|
||||
Refresh token expiry: ${refreshExpiry?.toLocaleString() || 'N/A'}
|
||||
Authenticated: ${tokenManager.isAuthenticated()}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ Token payload: ${JSON.stringify(payload, null, 2)}
|
||||
<pre className="bg-gray-100 dark:bg-gray-800 p-4 rounded text-sm">
|
||||
{getTokenInfo()}
|
||||
</pre>
|
||||
{refreshToken && (
|
||||
{isAuthenticated && (
|
||||
<p className="mt-2 text-sm text-green-600">
|
||||
Refresh token available - auto-refresh enabled
|
||||
Auto-refresh enabled - tokens will refresh automatically
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode, useRef } from "react"
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import {
|
||||
isTokenExpired,
|
||||
refreshAccessToken,
|
||||
storeTokens,
|
||||
getStoredTokens,
|
||||
clearTokens,
|
||||
setupTokenRefreshTimer,
|
||||
decodeToken
|
||||
} from "@/lib/auth-utils"
|
||||
import { tokenManager } from "@/lib/token-manager"
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
@@ -21,128 +13,107 @@ interface User {
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
token: string | null
|
||||
refreshToken: string | null
|
||||
isAuthenticated: boolean
|
||||
login: (email: string, password: string) => Promise<void>
|
||||
logout: () => void
|
||||
isLoading: boolean
|
||||
refreshTokenIfNeeded: () => Promise<boolean>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [refreshToken, setRefreshToken] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const router = useRouter()
|
||||
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Initialize auth state from localStorage
|
||||
// Initialize auth state and listen to token manager events
|
||||
useEffect(() => {
|
||||
const initAuth = async () => {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
// Don't try to refresh on auth-related pages
|
||||
const isAuthPage = window.location.pathname === '/login' ||
|
||||
window.location.pathname === '/register' ||
|
||||
window.location.pathname === '/forgot-password'
|
||||
|
||||
const { accessToken, refreshToken: storedRefreshToken } = getStoredTokens()
|
||||
|
||||
if (accessToken && storedRefreshToken) {
|
||||
// Check if token is expired
|
||||
if (isTokenExpired(accessToken)) {
|
||||
// Only try to refresh if not on auth pages
|
||||
if (!isAuthPage) {
|
||||
// Try to refresh the token
|
||||
const response = await refreshAccessToken(storedRefreshToken)
|
||||
if (response) {
|
||||
storeTokens(response.access_token, response.refresh_token)
|
||||
setToken(response.access_token)
|
||||
setRefreshToken(response.refresh_token)
|
||||
|
||||
// Decode token to get user info
|
||||
const payload = decodeToken(response.access_token)
|
||||
if (payload) {
|
||||
const storedUser = localStorage.getItem("user")
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser))
|
||||
// Check if we have valid tokens
|
||||
if (tokenManager.isAuthenticated()) {
|
||||
// Try to get user info
|
||||
await fetchUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup refresh timer
|
||||
setupRefreshTimer(response.access_token, response.refresh_token)
|
||||
} else {
|
||||
// Refresh failed, clear everything
|
||||
clearTokens()
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
setRefreshToken(null)
|
||||
}
|
||||
} else {
|
||||
// On auth pages with expired token, just clear it
|
||||
clearTokens()
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
setRefreshToken(null)
|
||||
}
|
||||
} else {
|
||||
// Token is still valid
|
||||
setToken(accessToken)
|
||||
setRefreshToken(storedRefreshToken)
|
||||
|
||||
const storedUser = localStorage.getItem("user")
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser))
|
||||
}
|
||||
|
||||
// Setup refresh timer
|
||||
setupRefreshTimer(accessToken, storedRefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
const handleTokensUpdated = () => {
|
||||
// Tokens were updated (refreshed), update user if needed
|
||||
if (!user) {
|
||||
fetchUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTokensCleared = () => {
|
||||
// Tokens were cleared, clear user
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
const handleSessionExpired = (reason: string) => {
|
||||
console.log('Session expired:', reason)
|
||||
setUser(null)
|
||||
// TokenManager and API client will handle redirect
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
setUser(null)
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// Register event listeners
|
||||
tokenManager.on('tokensUpdated', handleTokensUpdated)
|
||||
tokenManager.on('tokensCleared', handleTokensCleared)
|
||||
tokenManager.on('sessionExpired', handleSessionExpired)
|
||||
tokenManager.on('logout', handleLogout)
|
||||
|
||||
// Initialize
|
||||
initAuth()
|
||||
}, [])
|
||||
|
||||
// Setup token refresh timer
|
||||
const setupRefreshTimer = (accessToken: string, refreshTokenValue: string) => {
|
||||
// Clear existing timer
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current)
|
||||
}
|
||||
|
||||
refreshTimerRef.current = setupTokenRefreshTimer(
|
||||
accessToken,
|
||||
refreshTokenValue,
|
||||
(newAccessToken) => {
|
||||
setToken(newAccessToken)
|
||||
},
|
||||
() => {
|
||||
// Refresh failed, logout user
|
||||
logout()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Cleanup timer on unmount
|
||||
useEffect(() => {
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current)
|
||||
}
|
||||
tokenManager.off('tokensUpdated', handleTokensUpdated)
|
||||
tokenManager.off('tokensCleared', handleTokensCleared)
|
||||
tokenManager.off('sessionExpired', handleSessionExpired)
|
||||
tokenManager.off('logout', handleLogout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const token = await tokenManager.getAccessToken()
|
||||
if (!token) return
|
||||
|
||||
const response = await fetch('/api-internal/v1/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const userData = await response.json()
|
||||
const user = {
|
||||
id: userData.id || userData.sub,
|
||||
email: userData.email,
|
||||
name: userData.name || userData.email,
|
||||
role: userData.role || 'user',
|
||||
}
|
||||
setUser(user)
|
||||
|
||||
// Store user info for offline access
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Call real backend login endpoint
|
||||
const response = await fetch('/api-internal/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -158,38 +129,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Store tokens
|
||||
storeTokens(data.access_token, data.refresh_token)
|
||||
// Store tokens in TokenManager
|
||||
tokenManager.setTokens(
|
||||
data.access_token,
|
||||
data.refresh_token,
|
||||
data.expires_in
|
||||
)
|
||||
|
||||
// Decode token to get user info
|
||||
const payload = decodeToken(data.access_token)
|
||||
if (payload) {
|
||||
// Fetch user details
|
||||
const userResponse = await fetch('/api-internal/v1/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${data.access_token}`,
|
||||
},
|
||||
})
|
||||
// Fetch user info
|
||||
await fetchUserInfo()
|
||||
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json()
|
||||
const user = {
|
||||
id: userData.id || payload.sub,
|
||||
email: userData.email || payload.email || email,
|
||||
name: userData.name || userData.email || email,
|
||||
role: userData.role || 'user',
|
||||
}
|
||||
|
||||
localStorage.setItem("user", JSON.stringify(user))
|
||||
setUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
setToken(data.access_token)
|
||||
setRefreshToken(data.refresh_token)
|
||||
|
||||
// Setup refresh timer
|
||||
setupRefreshTimer(data.access_token, data.refresh_token)
|
||||
// Navigate to dashboard
|
||||
router.push('/dashboard')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
@@ -200,56 +151,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
// Clear refresh timer
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current)
|
||||
refreshTimerRef.current = null
|
||||
}
|
||||
|
||||
// Clear state
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
setRefreshToken(null)
|
||||
|
||||
// Clear localStorage
|
||||
clearTokens()
|
||||
|
||||
// Redirect to login
|
||||
router.push("/login")
|
||||
}
|
||||
|
||||
const refreshTokenIfNeeded = async (): Promise<boolean> => {
|
||||
if (!token || !refreshToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isTokenExpired(token)) {
|
||||
const response = await refreshAccessToken(refreshToken)
|
||||
if (response) {
|
||||
storeTokens(response.access_token, response.refresh_token)
|
||||
setToken(response.access_token)
|
||||
setRefreshToken(response.refresh_token)
|
||||
setupRefreshTimer(response.access_token, response.refresh_token)
|
||||
return true
|
||||
} else {
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
tokenManager.logout()
|
||||
// Token manager will emit 'logout' event which we handle above
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
token,
|
||||
refreshToken,
|
||||
isAuthenticated: tokenManager.isAuthenticated(),
|
||||
login,
|
||||
logout,
|
||||
isLoading,
|
||||
refreshTokenIfNeeded
|
||||
isLoading
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"
|
||||
import { apiClient } from "@/lib/api-client"
|
||||
import { tokenManager } from "@/lib/token-manager"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
interface Module {
|
||||
name: string
|
||||
@@ -34,11 +36,21 @@ const ModulesContext = createContext<ModulesContextType | undefined>(undefined)
|
||||
export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
const [modules, setModules] = useState<Module[]>([])
|
||||
const [enabledModules, setEnabledModules] = useState<Set<string>>(new Set())
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
||||
const pathname = usePathname()
|
||||
|
||||
// Check if we're on an auth page
|
||||
const isAuthPage = pathname === '/login' || pathname === '/register' || pathname === '/forgot-password'
|
||||
|
||||
const fetchModules = useCallback(async () => {
|
||||
// Don't fetch if we're on an auth page or not authenticated
|
||||
if (isAuthPage || !tokenManager.isAuthenticated()) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
@@ -57,11 +69,14 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
setLastUpdated(new Date())
|
||||
|
||||
} catch (err) {
|
||||
// Only set error if we're authenticated (to avoid noise on auth pages)
|
||||
if (tokenManager.isAuthenticated()) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load modules")
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [isAuthPage])
|
||||
|
||||
const refreshModules = useCallback(async () => {
|
||||
await fetchModules()
|
||||
@@ -72,13 +87,22 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
}, [enabledModules])
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch if authenticated and not on auth page
|
||||
if (!isAuthPage && tokenManager.isAuthenticated()) {
|
||||
fetchModules()
|
||||
}
|
||||
|
||||
// Set up periodic refresh every 30 seconds to catch module state changes
|
||||
const interval = setInterval(fetchModules, 30000)
|
||||
// But only if authenticated
|
||||
let interval: NodeJS.Timeout | null = null
|
||||
if (!isAuthPage && tokenManager.isAuthenticated()) {
|
||||
interval = setInterval(fetchModules, 30000)
|
||||
}
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchModules])
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [fetchModules, isAuthPage])
|
||||
|
||||
// Listen for custom events that indicate module state changes
|
||||
useEffect(() => {
|
||||
@@ -93,6 +117,34 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [refreshModules])
|
||||
|
||||
// Listen for authentication changes
|
||||
useEffect(() => {
|
||||
const handleTokensUpdated = () => {
|
||||
// When tokens are updated (user logs in), fetch modules
|
||||
if (!isAuthPage) {
|
||||
fetchModules()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTokensCleared = () => {
|
||||
// When tokens are cleared (user logs out), clear modules
|
||||
setModules([])
|
||||
setEnabledModules(new Set())
|
||||
setError(null)
|
||||
setLastUpdated(null)
|
||||
}
|
||||
|
||||
tokenManager.on('tokensUpdated', handleTokensUpdated)
|
||||
tokenManager.on('tokensCleared', handleTokensCleared)
|
||||
tokenManager.on('logout', handleTokensCleared)
|
||||
|
||||
return () => {
|
||||
tokenManager.off('tokensUpdated', handleTokensUpdated)
|
||||
tokenManager.off('tokensCleared', handleTokensCleared)
|
||||
tokenManager.off('logout', handleTokensCleared)
|
||||
}
|
||||
}, [fetchModules, isAuthPage])
|
||||
|
||||
return (
|
||||
<ModulesContext.Provider
|
||||
value={{
|
||||
|
||||
@@ -97,7 +97,7 @@ interface PluginProviderProps {
|
||||
}
|
||||
|
||||
export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
const { user, token } = useAuth();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const [installedPlugins, setInstalledPlugins] = useState<PluginInfo[]>([]);
|
||||
const [availablePlugins, setAvailablePlugins] = useState<AvailablePlugin[]>([]);
|
||||
const [pluginConfigurations, setPluginConfigurations] = useState<Record<string, PluginConfiguration>>({});
|
||||
@@ -108,7 +108,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
const [pluginComponents, setPluginComponents] = useState<Record<string, Record<string, React.ComponentType>>>({});
|
||||
|
||||
const apiRequest = async (endpoint: string, options: RequestInit = {}) => {
|
||||
if (!token) {
|
||||
if (!isAuthenticated) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
const refreshInstalledPlugins = useCallback(async () => {
|
||||
if (!user || !token) {
|
||||
if (!user || !isAuthenticated) {
|
||||
setError('Authentication required');
|
||||
return;
|
||||
}
|
||||
@@ -160,10 +160,10 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user, token]);
|
||||
}, [user, isAuthenticated]);
|
||||
|
||||
const searchAvailablePlugins = useCallback(async (query = '', tags: string[] = [], category = '') => {
|
||||
if (!user || !token) {
|
||||
if (!user || !isAuthenticated) {
|
||||
setError('Authentication required');
|
||||
return;
|
||||
}
|
||||
@@ -185,7 +185,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user, token]);
|
||||
}, [user, isAuthenticated]);
|
||||
|
||||
const installPlugin = useCallback(async (pluginId: string, version: string): Promise<boolean> => {
|
||||
try {
|
||||
@@ -487,7 +487,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
|
||||
// Load installed plugins on mount, but only when authenticated
|
||||
useEffect(() => {
|
||||
if (user && token) {
|
||||
if (user && isAuthenticated) {
|
||||
refreshInstalledPlugins();
|
||||
} else {
|
||||
// Clear plugin data when not authenticated
|
||||
@@ -496,7 +496,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||
setPluginConfigurations({});
|
||||
setError(null);
|
||||
}
|
||||
}, [user, token, refreshInstalledPlugins]);
|
||||
}, [user, isAuthenticated, refreshInstalledPlugins]);
|
||||
|
||||
const value: PluginContextType = {
|
||||
// State
|
||||
|
||||
Reference in New Issue
Block a user