simplifying the auth and creating strict separation

This commit is contained in:
2025-09-09 06:38:37 +02:00
parent bd7109e31b
commit 1b36a94034
16 changed files with 335 additions and 287 deletions

View File

@@ -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"])

View File

@@ -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"""

View 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"
)

View File

@@ -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})

View File

@@ -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)
):
"""

View File

@@ -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
]

View File

@@ -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:

View File

@@ -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
]

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>

View File

@@ -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}

View File

@@ -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={{

View File

@@ -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