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.security import router as security_router
|
||||||
from ..v1.plugin_registry import router as plugin_registry_router
|
from ..v1.plugin_registry import router as plugin_registry_router
|
||||||
from ..v1.platform import router as platform_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
|
from ..v1.chatbot import router as chatbot_router
|
||||||
|
|
||||||
# Create internal API 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)
|
# Include plugin registry routes (frontend plugin management)
|
||||||
internal_api_router.include_router(plugin_registry_router, prefix="/plugins", tags=["internal-plugins"])
|
internal_api_router.include_router(plugin_registry_router, prefix="/plugins", tags=["internal-plugins"])
|
||||||
|
|
||||||
# Include LLM routes (frontend LLM service access)
|
# Include internal LLM routes (frontend LLM service access with JWT auth)
|
||||||
internal_api_router.include_router(llm_router, prefix="/llm", tags=["internal-llm"])
|
internal_api_router.include_router(llm_internal_router, prefix="/llm", tags=["internal-llm"])
|
||||||
|
|
||||||
# Include chatbot routes (frontend chatbot management)
|
# Include chatbot routes (frontend chatbot management)
|
||||||
internal_api_router.include_router(chatbot_router, prefix="/chatbot", tags=["internal-chatbot"])
|
internal_api_router.include_router(chatbot_router, prefix="/chatbot", tags=["internal-chatbot"])
|
||||||
@@ -126,62 +126,13 @@ class ModelsResponse(BaseModel):
|
|||||||
data: List[ModelInfo]
|
data: List[ModelInfo]
|
||||||
|
|
||||||
|
|
||||||
# Hybrid authentication function
|
# Authentication: Public API endpoints should use require_api_key
|
||||||
async def get_auth_context(
|
# Internal API endpoints should use get_current_user from core.security
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
@router.get("/models", response_model=ModelsResponse)
|
@router.get("/models", response_model=ModelsResponse)
|
||||||
async def list_models(
|
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)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""List available models"""
|
"""List available models"""
|
||||||
@@ -220,7 +171,7 @@ async def list_models(
|
|||||||
|
|
||||||
@router.post("/models/invalidate-cache")
|
@router.post("/models/invalidate-cache")
|
||||||
async def invalidate_models_cache_endpoint(
|
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)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Invalidate models cache (admin only)"""
|
"""Invalidate models cache (admin only)"""
|
||||||
@@ -249,7 +200,7 @@ async def invalidate_models_cache_endpoint(
|
|||||||
async def create_chat_completion(
|
async def create_chat_completion(
|
||||||
request_body: Request,
|
request_body: Request,
|
||||||
chat_request: ChatCompletionRequest,
|
chat_request: ChatCompletionRequest,
|
||||||
context: Dict[str, Any] = Depends(get_auth_context),
|
context: Dict[str, Any] = Depends(require_api_key),
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Create chat completion with budget enforcement"""
|
"""Create chat completion with budget enforcement"""
|
||||||
@@ -604,7 +555,7 @@ async def create_embedding(
|
|||||||
|
|
||||||
@router.get("/health")
|
@router.get("/health")
|
||||||
async def llm_health_check(
|
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"""
|
"""Health check for LLM service"""
|
||||||
try:
|
try:
|
||||||
@@ -686,7 +637,7 @@ async def get_usage_stats(
|
|||||||
@router.get("/budget/status")
|
@router.get("/budget/status")
|
||||||
async def get_budget_status(
|
async def get_budget_status(
|
||||||
request: Request,
|
request: Request,
|
||||||
context: Dict[str, Any] = Depends(get_auth_context),
|
context: Dict[str, Any] = Depends(require_api_key),
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get current budget status and usage analytics"""
|
"""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 fastapi import APIRouter, Depends, HTTPException
|
||||||
from app.services.module_manager import module_manager, ModuleConfig
|
from app.services.module_manager import module_manager, ModuleConfig
|
||||||
from app.core.logging import log_api_request
|
from app.core.logging import log_api_request
|
||||||
|
from app.core.security import get_current_user
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@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)"""
|
"""Get list of all discovered modules with their status (enabled and disabled)"""
|
||||||
log_api_request("list_modules", {})
|
log_api_request("list_modules", {})
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ async def list_modules():
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@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"""
|
"""Get comprehensive module status - CONSOLIDATED endpoint"""
|
||||||
log_api_request("get_modules_status", {})
|
log_api_request("get_modules_status", {})
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ async def get_modules_status():
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/{module_name}")
|
@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"""
|
"""Get detailed information about a specific module"""
|
||||||
log_api_request("get_module_info", {"module_name": module_name})
|
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")
|
@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"""
|
"""Enable a module"""
|
||||||
log_api_request("enable_module", {"module_name": module_name})
|
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")
|
@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"""
|
"""Disable a module"""
|
||||||
log_api_request("disable_module", {"module_name": module_name})
|
log_api_request("disable_module", {"module_name": module_name})
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ async def disable_module(module_name: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/all/reload")
|
@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"""
|
"""Reload all modules"""
|
||||||
log_api_request("reload_all_modules", {})
|
log_api_request("reload_all_modules", {})
|
||||||
|
|
||||||
@@ -271,7 +272,7 @@ async def reload_all_modules():
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/{module_name}/reload")
|
@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"""
|
"""Reload a specific module"""
|
||||||
log_api_request("reload_module", {"module_name": module_name})
|
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")
|
@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)"""
|
"""Restart a specific module (alias for reload)"""
|
||||||
log_api_request("restart_module", {"module_name": module_name})
|
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")
|
@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)"""
|
"""Start a specific module (enable and load)"""
|
||||||
log_api_request("start_module", {"module_name": module_name})
|
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")
|
@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)"""
|
"""Stop a specific module (disable and unload)"""
|
||||||
log_api_request("stop_module", {"module_name": module_name})
|
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")
|
@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"""
|
"""Get module statistics"""
|
||||||
log_api_request("get_module_stats", {"module_name": module_name})
|
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")
|
@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"""
|
"""Execute a module action through the interceptor pattern"""
|
||||||
log_api_request("execute_module_action", {"module_name": module_name, "action": request_data.get("action")})
|
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")
|
@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"""
|
"""Get module configuration schema and current values"""
|
||||||
log_api_request("get_module_config", {"module_name": module_name})
|
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")
|
@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"""
|
"""Update module configuration"""
|
||||||
log_api_request("update_module_config", {"module_name": module_name})
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.db.database import get_db
|
from app.db.database import get_db
|
||||||
|
from app.services.api_key_auth import require_api_key
|
||||||
from app.api.v1.llm import (
|
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,
|
ChatCompletionRequest, EmbeddingRequest, create_chat_completion as llm_chat_completion,
|
||||||
create_embedding as llm_create_embedding
|
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)
|
@router.get("/models", response_model=ModelsResponse)
|
||||||
async def list_models(
|
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)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -84,7 +85,7 @@ async def list_models(
|
|||||||
async def create_chat_completion(
|
async def create_chat_completion(
|
||||||
request_body: Request,
|
request_body: Request,
|
||||||
chat_request: ChatCompletionRequest,
|
chat_request: ChatCompletionRequest,
|
||||||
context: Dict[str, Any] = Depends(get_auth_context),
|
context: Dict[str, Any] = Depends(require_api_key),
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -100,7 +101,7 @@ async def create_chat_completion(
|
|||||||
@router.post("/embeddings")
|
@router.post("/embeddings")
|
||||||
async def create_embedding(
|
async def create_embedding(
|
||||||
request: EmbeddingRequest,
|
request: EmbeddingRequest,
|
||||||
context: Dict[str, Any] = Depends(get_auth_context),
|
context: Dict[str, Any] = Depends(require_api_key),
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -116,7 +117,7 @@ async def create_embedding(
|
|||||||
@router.get("/models/{model_id}")
|
@router.get("/models/{model_id}")
|
||||||
async def retrieve_model(
|
async def retrieve_model(
|
||||||
model_id: str,
|
model_id: str,
|
||||||
context: Dict[str, Any] = Depends(get_auth_context),
|
context: Dict[str, Any] = Depends(require_api_key),
|
||||||
db: AsyncSession = Depends(get_db)
|
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.services.permission_manager import permission_registry, Permission, PermissionScope
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
|
from app.core.security import get_current_user
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ async def get_available_permissions(namespace: Optional[str] = None):
|
|||||||
resource=perm.resource,
|
resource=perm.resource,
|
||||||
action=perm.action,
|
action=perm.action,
|
||||||
description=perm.description,
|
description=perm.description,
|
||||||
conditions=perm.conditions
|
conditions=getattr(perm, 'conditions', None)
|
||||||
)
|
)
|
||||||
for perm in perms
|
for perm in perms
|
||||||
]
|
]
|
||||||
@@ -131,7 +132,10 @@ async def validate_permissions(request: PermissionValidationRequest):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/permissions/check", response_model=PermissionCheckResponse)
|
@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"""
|
"""Check if user has a specific permission"""
|
||||||
try:
|
try:
|
||||||
has_permission = permission_registry.check_permission(
|
has_permission = permission_registry.check_permission(
|
||||||
@@ -168,7 +172,7 @@ async def get_module_permissions(module_id: str):
|
|||||||
resource=perm.resource,
|
resource=perm.resource,
|
||||||
action=perm.action,
|
action=perm.action,
|
||||||
description=perm.description,
|
description=perm.description,
|
||||||
conditions=perm.conditions
|
conditions=getattr(perm, 'conditions', None)
|
||||||
)
|
)
|
||||||
for perm in permissions
|
for perm in permissions
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
except Exception as e:
|
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()
|
await session.rollback()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -104,8 +104,10 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
|||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
"/api/v1/auth/register",
|
"/api/v1/auth/register",
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
|
"/api/v1/auth/refresh", # Allow refresh endpoint
|
||||||
"/api-internal/v1/auth/register",
|
"/api-internal/v1/auth/register",
|
||||||
"/api-internal/v1/auth/login",
|
"/api-internal/v1/auth/login",
|
||||||
|
"/api-internal/v1/auth/refresh", # Allow refresh endpoint for internal API
|
||||||
"/", # Root endpoint
|
"/", # Root endpoint
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Handles authentication, routing, and security for plugin APIs
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
import jwt
|
from jose import jwt
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Plugin Security and Authentication Service
|
Plugin Security and Authentication Service
|
||||||
Handles plugin tokens, permissions, and security policies
|
Handles plugin tokens, permissions, and security policies
|
||||||
"""
|
"""
|
||||||
import jwt
|
from jose import jwt
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
|||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"next": "^14.2.32",
|
"next": "^14.2.32",
|
||||||
@@ -3973,6 +3974,15 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"next": "^14.2.32",
|
"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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { apiClient } from "@/lib/api-client"
|
import { apiClient } from "@/lib/api-client"
|
||||||
import { decodeToken } from "@/lib/auth-utils"
|
import { tokenManager } from "@/lib/token-manager"
|
||||||
|
|
||||||
export default function TestAuthPage() {
|
export default function TestAuthPage() {
|
||||||
const { user, token, refreshToken, logout } = useAuth()
|
const { user, logout, isAuthenticated } = useAuth()
|
||||||
const [testResult, setTestResult] = useState<string>("")
|
const [testResult, setTestResult] = useState<string>("")
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
@@ -24,19 +24,19 @@ export default function TestAuthPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTokenInfo = () => {
|
const getTokenInfo = () => {
|
||||||
if (!token) return "No token"
|
const expiry = tokenManager.getTokenExpiry()
|
||||||
|
const refreshExpiry = tokenManager.getRefreshTokenExpiry()
|
||||||
|
|
||||||
const payload = decodeToken(token)
|
if (!expiry) return "No token"
|
||||||
if (!payload) return "Invalid token"
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000)
|
const now = new Date()
|
||||||
const timeUntilExpiry = payload.exp - now
|
const timeUntilExpiry = Math.floor((expiry.getTime() - now.getTime()) / 1000)
|
||||||
const expiryDate = new Date(payload.exp * 1000)
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
Token expires in: ${Math.floor(timeUntilExpiry / 60)} minutes ${timeUntilExpiry % 60} seconds
|
Token expires in: ${Math.floor(timeUntilExpiry / 60)} minutes ${timeUntilExpiry % 60} seconds
|
||||||
Expiry time: ${expiryDate.toLocaleString()}
|
Access token expiry: ${expiry.toLocaleString()}
|
||||||
Token payload: ${JSON.stringify(payload, null, 2)}
|
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">
|
<pre className="bg-gray-100 dark:bg-gray-800 p-4 rounded text-sm">
|
||||||
{getTokenInfo()}
|
{getTokenInfo()}
|
||||||
</pre>
|
</pre>
|
||||||
{refreshToken && (
|
{isAuthenticated && (
|
||||||
<p className="mt-2 text-sm text-green-600">
|
<p className="mt-2 text-sm text-green-600">
|
||||||
Refresh token available - auto-refresh enabled
|
Auto-refresh enabled - tokens will refresh automatically
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
"use client"
|
"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 { useRouter } from "next/navigation"
|
||||||
import {
|
import { tokenManager } from "@/lib/token-manager"
|
||||||
isTokenExpired,
|
|
||||||
refreshAccessToken,
|
|
||||||
storeTokens,
|
|
||||||
getStoredTokens,
|
|
||||||
clearTokens,
|
|
||||||
setupTokenRefreshTimer,
|
|
||||||
decodeToken
|
|
||||||
} from "@/lib/auth-utils"
|
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
@@ -21,128 +13,107 @@ interface User {
|
|||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null
|
user: User | null
|
||||||
token: string | null
|
isAuthenticated: boolean
|
||||||
refreshToken: string | null
|
|
||||||
login: (email: string, password: string) => Promise<void>
|
login: (email: string, password: string) => Promise<void>
|
||||||
logout: () => void
|
logout: () => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
refreshTokenIfNeeded: () => Promise<boolean>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
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 [isLoading, setIsLoading] = useState(true)
|
||||||
const router = useRouter()
|
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(() => {
|
useEffect(() => {
|
||||||
const initAuth = async () => {
|
const initAuth = async () => {
|
||||||
if (typeof window === "undefined") return
|
// Check if we have valid tokens
|
||||||
|
if (tokenManager.isAuthenticated()) {
|
||||||
// Don't try to refresh on auth-related pages
|
// Try to get user info
|
||||||
const isAuthPage = window.location.pathname === '/login' ||
|
await fetchUserInfo()
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
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()
|
initAuth()
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Setup token refresh timer
|
// Cleanup
|
||||||
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(() => {
|
|
||||||
return () => {
|
return () => {
|
||||||
if (refreshTimerRef.current) {
|
tokenManager.off('tokensUpdated', handleTokensUpdated)
|
||||||
clearTimeout(refreshTimerRef.current)
|
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) => {
|
const login = async (email: string, password: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call real backend login endpoint
|
|
||||||
const response = await fetch('/api-internal/v1/auth/login', {
|
const response = await fetch('/api-internal/v1/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -158,38 +129,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
// Store tokens
|
// Store tokens in TokenManager
|
||||||
storeTokens(data.access_token, data.refresh_token)
|
tokenManager.setTokens(
|
||||||
|
data.access_token,
|
||||||
|
data.refresh_token,
|
||||||
|
data.expires_in
|
||||||
|
)
|
||||||
|
|
||||||
// Decode token to get user info
|
// Fetch user info
|
||||||
const payload = decodeToken(data.access_token)
|
await fetchUserInfo()
|
||||||
if (payload) {
|
|
||||||
// Fetch user details
|
|
||||||
const userResponse = await fetch('/api-internal/v1/auth/me', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${data.access_token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
// Navigate to dashboard
|
||||||
setRefreshToken(data.refresh_token)
|
router.push('/dashboard')
|
||||||
|
|
||||||
// Setup refresh timer
|
|
||||||
setupRefreshTimer(data.access_token, data.refresh_token)
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error)
|
console.error('Login error:', error)
|
||||||
@@ -200,56 +151,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
// Clear refresh timer
|
tokenManager.logout()
|
||||||
if (refreshTimerRef.current) {
|
// Token manager will emit 'logout' event which we handle above
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
token,
|
isAuthenticated: tokenManager.isAuthenticated(),
|
||||||
refreshToken,
|
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
isLoading,
|
isLoading
|
||||||
refreshTokenIfNeeded
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"
|
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"
|
||||||
import { apiClient } from "@/lib/api-client"
|
import { apiClient } from "@/lib/api-client"
|
||||||
|
import { tokenManager } from "@/lib/token-manager"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
|
||||||
interface Module {
|
interface Module {
|
||||||
name: string
|
name: string
|
||||||
@@ -34,11 +36,21 @@ const ModulesContext = createContext<ModulesContextType | undefined>(undefined)
|
|||||||
export function ModulesProvider({ children }: { children: ReactNode }) {
|
export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||||
const [modules, setModules] = useState<Module[]>([])
|
const [modules, setModules] = useState<Module[]>([])
|
||||||
const [enabledModules, setEnabledModules] = useState<Set<string>>(new Set())
|
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 [error, setError] = useState<string | null>(null)
|
||||||
const [lastUpdated, setLastUpdated] = useState<Date | 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 () => {
|
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 {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -57,11 +69,14 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
|||||||
setLastUpdated(new Date())
|
setLastUpdated(new Date())
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to load modules")
|
// 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 {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [isAuthPage])
|
||||||
|
|
||||||
const refreshModules = useCallback(async () => {
|
const refreshModules = useCallback(async () => {
|
||||||
await fetchModules()
|
await fetchModules()
|
||||||
@@ -72,13 +87,22 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [enabledModules])
|
}, [enabledModules])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModules()
|
// 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
|
// 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)
|
return () => {
|
||||||
}, [fetchModules])
|
if (interval) clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [fetchModules, isAuthPage])
|
||||||
|
|
||||||
// Listen for custom events that indicate module state changes
|
// Listen for custom events that indicate module state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,6 +117,34 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [refreshModules])
|
}, [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 (
|
return (
|
||||||
<ModulesContext.Provider
|
<ModulesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ interface PluginProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
||||||
const { user, token } = useAuth();
|
const { user, isAuthenticated } = useAuth();
|
||||||
const [installedPlugins, setInstalledPlugins] = useState<PluginInfo[]>([]);
|
const [installedPlugins, setInstalledPlugins] = useState<PluginInfo[]>([]);
|
||||||
const [availablePlugins, setAvailablePlugins] = useState<AvailablePlugin[]>([]);
|
const [availablePlugins, setAvailablePlugins] = useState<AvailablePlugin[]>([]);
|
||||||
const [pluginConfigurations, setPluginConfigurations] = useState<Record<string, PluginConfiguration>>({});
|
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 [pluginComponents, setPluginComponents] = useState<Record<string, Record<string, React.ComponentType>>>({});
|
||||||
|
|
||||||
const apiRequest = async (endpoint: string, options: RequestInit = {}) => {
|
const apiRequest = async (endpoint: string, options: RequestInit = {}) => {
|
||||||
if (!token) {
|
if (!isAuthenticated) {
|
||||||
throw new Error('Authentication required');
|
throw new Error('Authentication required');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refreshInstalledPlugins = useCallback(async () => {
|
const refreshInstalledPlugins = useCallback(async () => {
|
||||||
if (!user || !token) {
|
if (!user || !isAuthenticated) {
|
||||||
setError('Authentication required');
|
setError('Authentication required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -160,10 +160,10 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, token]);
|
}, [user, isAuthenticated]);
|
||||||
|
|
||||||
const searchAvailablePlugins = useCallback(async (query = '', tags: string[] = [], category = '') => {
|
const searchAvailablePlugins = useCallback(async (query = '', tags: string[] = [], category = '') => {
|
||||||
if (!user || !token) {
|
if (!user || !isAuthenticated) {
|
||||||
setError('Authentication required');
|
setError('Authentication required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, token]);
|
}, [user, isAuthenticated]);
|
||||||
|
|
||||||
const installPlugin = useCallback(async (pluginId: string, version: string): Promise<boolean> => {
|
const installPlugin = useCallback(async (pluginId: string, version: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
@@ -487,7 +487,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
// Load installed plugins on mount, but only when authenticated
|
// Load installed plugins on mount, but only when authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && token) {
|
if (user && isAuthenticated) {
|
||||||
refreshInstalledPlugins();
|
refreshInstalledPlugins();
|
||||||
} else {
|
} else {
|
||||||
// Clear plugin data when not authenticated
|
// Clear plugin data when not authenticated
|
||||||
@@ -496,7 +496,7 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children }) => {
|
|||||||
setPluginConfigurations({});
|
setPluginConfigurations({});
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, [user, token, refreshInstalledPlugins]);
|
}, [user, isAuthenticated, refreshInstalledPlugins]);
|
||||||
|
|
||||||
const value: PluginContextType = {
|
const value: PluginContextType = {
|
||||||
// State
|
// State
|
||||||
|
|||||||
Reference in New Issue
Block a user