Implement complete Bearer Token and API key authentication system

- Create comprehensive authentication backend with JWT and API key support
- Add database models for users, API keys, and tokens with proper security
- Implement authentication middleware and API endpoints
- Build complete frontend authentication UI with:
  - LoginForm component with JWT authentication
  - APIKeyManager for creating and managing API keys
  - AuthDashboard for comprehensive auth management
  - AuthContext for state management and authenticated requests
- Initialize database with default admin user (admin/admin123)
- Add proper token refresh, validation, and blacklisting
- Implement scope-based API key authorization system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-10 20:52:58 +10:00
parent d19c9d596f
commit 7af5b47477
10 changed files with 2535 additions and 73 deletions

449
backend/app/api/auth.py Normal file
View File

@@ -0,0 +1,449 @@
"""
Authentication API endpoints for Hive platform.
Handles user registration, login, token refresh, and API key management.
"""
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr
from app.core.database import get_db
from app.core.security import TokenManager, APIKeyManager, create_token_response, verify_password
from app.core.auth_deps import (
get_current_user_context,
get_current_active_user,
get_current_superuser,
require_admin
)
from app.models.auth import User, APIKey, RefreshToken, TokenBlacklist, API_SCOPES, DEFAULT_API_SCOPES
router = APIRouter()
# Pydantic models for request/response
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
full_name: Optional[str] = None
class UserResponse(BaseModel):
id: int
username: str
email: str
full_name: Optional[str]
is_active: bool
is_superuser: bool
is_verified: bool
created_at: str
last_login: Optional[str]
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str
expires_in: int
user: UserResponse
class RefreshTokenRequest(BaseModel):
refresh_token: str
class APIKeyCreate(BaseModel):
name: str
scopes: Optional[List[str]] = None
expires_at: Optional[datetime] = None
class APIKeyResponse(BaseModel):
id: int
name: str
key_prefix: str
scopes: List[str]
is_active: bool
last_used: Optional[str]
usage_count: int
expires_at: Optional[str]
created_at: str
class APIKeyCreateResponse(APIKeyResponse):
api_key: str # Only returned once during creation
class PasswordChange(BaseModel):
current_password: str
new_password: str
class ScopeInfo(BaseModel):
scope: str
description: str
# Authentication endpoints
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register_user(
user_data: UserCreate,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_superuser) # Only admins can create users
):
"""Register a new user (admin only)."""
# Check if username or email already exists
existing_user = db.query(User).filter(
(User.username == user_data.username) | (User.email == user_data.email)
).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username or email already registered"
)
# Create new user
user = User(
username=user_data.username,
email=user_data.email,
full_name=user_data.full_name,
hashed_password=User.hash_password(user_data.password),
is_active=True,
is_verified=True # Auto-verify admin-created users
)
db.add(user)
db.commit()
db.refresh(user)
return UserResponse(**user.to_dict())
@router.post("/login", response_model=TokenResponse)
async def login(
request: Request,
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
"""Authenticate user and return JWT tokens."""
# Find user by username
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not user.verify_password(form_data.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
# Update last login
user.update_last_login()
db.commit()
# Create token response
user_data = user.to_dict()
user_data["scopes"] = ["admin"] if user.is_superuser else []
token_response = create_token_response(user.id, user_data)
# Store refresh token in database
refresh_token_plain = token_response["refresh_token"]
refresh_token_hash = User.hash_password(refresh_token_plain)
# Get device info
device_info = {
"user_agent": request.headers.get("user-agent", ""),
"ip": request.client.host if request.client else None,
}
# Create refresh token record
refresh_token_record = RefreshToken(
user_id=user.id,
token_hash=refresh_token_hash,
jti=TokenManager.get_token_claims(refresh_token_plain).get("jti"),
device_info=str(device_info),
expires_at=datetime.utcnow() + timedelta(days=30)
)
db.add(refresh_token_record)
db.commit()
return TokenResponse(**token_response)
@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(
refresh_request: RefreshTokenRequest,
db: Session = Depends(get_db)
):
"""Refresh access token using refresh token."""
try:
# Verify refresh token
payload = TokenManager.verify_token(refresh_request.refresh_token)
if payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type"
)
user_id = int(payload.get("sub"))
jti = payload.get("jti")
# Check if refresh token exists and is valid
refresh_token_record = db.query(RefreshToken).filter(
RefreshToken.jti == jti,
RefreshToken.user_id == user_id,
RefreshToken.is_active == True
).first()
if not refresh_token_record or not refresh_token_record.is_valid():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token"
)
# Get user
user = db.query(User).filter(User.id == user_id).first()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive"
)
# Update refresh token usage
refresh_token_record.record_usage()
db.commit()
# Create new token response
user_data = user.to_dict()
user_data["scopes"] = ["admin"] if user.is_superuser else []
return TokenResponse(**create_token_response(user.id, user_data))
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate refresh token"
)
@router.post("/logout")
async def logout(
current_user: Dict[str, Any] = Depends(get_current_user_context),
db: Session = Depends(get_db)
):
"""Logout user and revoke current tokens."""
# Blacklist the current access token
if current_user.get("token_jti"):
TokenBlacklist.blacklist_token(
db,
current_user["token_jti"],
"access",
datetime.utcnow() + timedelta(hours=1) # Token would expire anyway
)
# Revoke all user's refresh tokens
refresh_tokens = db.query(RefreshToken).filter(
RefreshToken.user_id == current_user["user_id"],
RefreshToken.is_active == True
).all()
for token in refresh_tokens:
token.revoke()
db.commit()
return {"message": "Successfully logged out"}
# User management endpoints
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: Dict[str, Any] = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get current user information."""
user = db.query(User).filter(User.id == current_user["user_id"]).first()
return UserResponse(**user.to_dict())
@router.post("/change-password")
async def change_password(
password_data: PasswordChange,
current_user: Dict[str, Any] = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Change current user's password."""
user = db.query(User).filter(User.id == current_user["user_id"]).first()
if not user.verify_password(password_data.current_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect"
)
user.set_password(password_data.new_password)
db.commit()
return {"message": "Password changed successfully"}
# API Key management endpoints
@router.get("/api-keys", response_model=List[APIKeyResponse])
async def list_api_keys(
current_user: Dict[str, Any] = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""List user's API keys."""
api_keys = db.query(APIKey).filter(APIKey.user_id == current_user["user_id"]).all()
return [APIKeyResponse(**key.to_dict()) for key in api_keys]
@router.post("/api-keys", response_model=APIKeyCreateResponse)
async def create_api_key(
key_data: APIKeyCreate,
current_user: Dict[str, Any] = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Create a new API key."""
# Generate API key
plain_key, hashed_key, prefix = APIKeyManager.generate_api_key()
# Set default scopes if none provided
scopes = key_data.scopes if key_data.scopes else DEFAULT_API_SCOPES
# Validate scopes
invalid_scopes = [scope for scope in scopes if scope not in API_SCOPES]
if invalid_scopes:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid scopes: {', '.join(invalid_scopes)}"
)
# Create API key record
api_key = APIKey(
user_id=current_user["user_id"],
name=key_data.name,
key_hash=hashed_key,
key_prefix=prefix,
expires_at=key_data.expires_at
)
api_key.set_scopes(scopes)
db.add(api_key)
db.commit()
db.refresh(api_key)
# Return API key with the plain key (only time it's shown)
response_data = api_key.to_dict()
response_data["api_key"] = plain_key
return APIKeyCreateResponse(**response_data)
@router.delete("/api-keys/{key_id}")
async def delete_api_key(
key_id: int,
current_user: Dict[str, Any] = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Delete an API key."""
api_key = db.query(APIKey).filter(
APIKey.id == key_id,
APIKey.user_id == current_user["user_id"]
).first()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
)
db.delete(api_key)
db.commit()
return {"message": "API key deleted successfully"}
@router.patch("/api-keys/{key_id}")
async def update_api_key(
key_id: int,
key_data: dict,
current_user: Dict[str, Any] = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Update an API key (name, scopes, active status)."""
api_key = db.query(APIKey).filter(
APIKey.id == key_id,
APIKey.user_id == current_user["user_id"]
).first()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
)
# Update allowed fields
if "name" in key_data:
api_key.name = key_data["name"]
if "scopes" in key_data:
scopes = key_data["scopes"]
invalid_scopes = [scope for scope in scopes if scope not in API_SCOPES]
if invalid_scopes:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid scopes: {', '.join(invalid_scopes)}"
)
api_key.set_scopes(scopes)
if "is_active" in key_data:
api_key.is_active = key_data["is_active"]
db.commit()
return APIKeyResponse(**api_key.to_dict())
# Admin endpoints
@router.get("/users", response_model=List[UserResponse])
async def list_users(
current_user: Dict[str, Any] = Depends(require_admin),
db: Session = Depends(get_db)
):
"""List all users (admin only)."""
users = db.query(User).all()
return [UserResponse(**user.to_dict()) for user in users]
@router.get("/scopes", response_model=List[ScopeInfo])
async def list_available_scopes():
"""List all available API scopes."""
return [
ScopeInfo(scope=scope, description=description)
for scope, description in API_SCOPES.items()
]
@router.post("/cleanup-tokens")
async def cleanup_expired_tokens(
current_user: Dict[str, Any] = Depends(require_admin),
db: Session = Depends(get_db)
):
"""Cleanup expired tokens from blacklist (admin only)."""
count = TokenBlacklist.cleanup_expired_tokens(db)
return {"message": f"Cleaned up {count} expired tokens"}

View File

@@ -0,0 +1,199 @@
"""
Authentication dependencies for FastAPI endpoints.
Provides dependency injection for authentication and authorization.
"""
from typing import Optional, Dict, Any, List
from fastapi import Depends, HTTPException, status, Request, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import AuthManager, AuthenticationError
security = HTTPBearer(auto_error=False)
def get_api_key_from_header(x_api_key: Optional[str] = Header(None)) -> Optional[str]:
"""Extract API key from X-API-Key header."""
return x_api_key
def get_current_user_context(
request: Request,
db: Session = Depends(get_db),
authorization: Optional[HTTPAuthorizationCredentials] = Depends(security),
api_key: Optional[str] = Depends(get_api_key_from_header),
) -> Dict[str, Any]:
"""
Get current authenticated user context.
Supports both JWT Bearer tokens and API key authentication.
"""
try:
user_context = AuthManager.authenticate_request(
session=db,
authorization=authorization,
api_key=api_key
)
# Add request metadata
user_context["request_ip"] = request.client.host if request.client else None
user_context["user_agent"] = request.headers.get("user-agent")
return user_context
except AuthenticationError as e:
raise HTTPException(
status_code=e.status_code,
detail=e.message,
headers={"WWW-Authenticate": "Bearer"},
)
def get_current_user(
user_context: Dict[str, Any] = Depends(get_current_user_context),
) -> Dict[str, Any]:
"""Get current authenticated user (alias for get_current_user_context)."""
return user_context
def get_current_active_user(
user_context: Dict[str, Any] = Depends(get_current_user_context),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""Get current authenticated and active user."""
from app.models.auth import User
user = db.query(User).filter(User.id == user_context["user_id"]).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return user_context
def get_current_superuser(
user_context: Dict[str, Any] = Depends(get_current_active_user),
) -> Dict[str, Any]:
"""Get current authenticated superuser."""
if not user_context.get("is_superuser", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return user_context
def require_scope(required_scope: str):
"""
Dependency factory to require specific scope for an endpoint.
Usage:
@app.get("/admin/users", dependencies=[Depends(require_scope("admin"))])
async def get_users():
...
"""
def scope_dependency(
user_context: Dict[str, Any] = Depends(get_current_active_user)
) -> Dict[str, Any]:
from app.core.security import APIKeyManager
user_scopes = user_context.get("scopes", [])
if not APIKeyManager.check_scope_permission(user_scopes, required_scope):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Insufficient permissions. Required scope: {required_scope}"
)
return user_context
return scope_dependency
def require_scopes(required_scopes: List[str]):
"""
Dependency factory to require multiple scopes for an endpoint.
User must have ALL specified scopes.
"""
def scopes_dependency(
user_context: Dict[str, Any] = Depends(get_current_active_user)
) -> Dict[str, Any]:
from app.core.security import APIKeyManager
user_scopes = user_context.get("scopes", [])
for scope in required_scopes:
if not APIKeyManager.check_scope_permission(user_scopes, scope):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Insufficient permissions. Required scopes: {', '.join(required_scopes)}"
)
return user_context
return scopes_dependency
def require_any_scope(required_scopes: List[str]):
"""
Dependency factory to require at least one of the specified scopes.
User must have ANY of the specified scopes.
"""
def any_scope_dependency(
user_context: Dict[str, Any] = Depends(get_current_active_user)
) -> Dict[str, Any]:
from app.core.security import APIKeyManager
user_scopes = user_context.get("scopes", [])
for scope in required_scopes:
if APIKeyManager.check_scope_permission(user_scopes, scope):
return user_context
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Insufficient permissions. Required one of: {', '.join(required_scopes)}"
)
return any_scope_dependency
# Optional authentication (won't raise error if not authenticated)
def get_optional_user_context(
db: Session = Depends(get_db),
authorization: Optional[HTTPAuthorizationCredentials] = Depends(security),
api_key: Optional[str] = Depends(get_api_key_from_header),
) -> Optional[Dict[str, Any]]:
"""
Get current user context if authenticated, None otherwise.
Useful for endpoints that work with or without authentication.
"""
try:
return AuthManager.authenticate_request(
session=db,
authorization=authorization,
api_key=api_key
)
except AuthenticationError:
return None
# Common scope dependencies for convenience
require_admin = require_scope("admin")
require_agents_read = require_scope("agents:read")
require_agents_write = require_scope("agents:write")
require_workflows_read = require_scope("workflows:read")
require_workflows_write = require_scope("workflows:write")
require_tasks_read = require_scope("tasks:read")
require_tasks_write = require_scope("tasks:write")
require_metrics_read = require_scope("metrics:read")
require_system_read = require_scope("system:read")
require_system_write = require_scope("system:write")

127
backend/app/core/init_db.py Normal file
View File

@@ -0,0 +1,127 @@
"""
Database initialization script for Hive platform.
Creates all tables and sets up initial data.
"""
import logging
from sqlalchemy.orm import Session
from app.core.database import engine, SessionLocal
from app.models.auth import Base as AuthBase, User, API_SCOPES
from app.models.auth import APIKey
# Import other model bases here as they're created
# from app.models.workflows import Base as WorkflowsBase
# from app.models.agents import Base as AgentsBase
def create_tables():
"""Create all database tables."""
try:
# Create auth tables
AuthBase.metadata.create_all(bind=engine)
# Add other model bases here
# WorkflowsBase.metadata.create_all(bind=engine)
# AgentsBase.metadata.create_all(bind=engine)
logging.info("Database tables created successfully")
return True
except Exception as e:
logging.error(f"Failed to create database tables: {e}")
return False
def create_initial_user(db: Session):
"""Create initial admin user if none exists."""
try:
# Check if any users exist
user_count = db.query(User).count()
if user_count > 0:
logging.info("Users already exist, skipping initial user creation")
return True
# Create initial admin user
admin_user = User(
username="admin",
email="admin@hive.local",
full_name="Hive Administrator",
hashed_password=User.hash_password("admin123"), # Change this!
is_active=True,
is_superuser=True,
is_verified=True
)
db.add(admin_user)
db.commit()
db.refresh(admin_user)
logging.info("Initial admin user created: admin/admin123")
logging.warning("SECURITY: Please change the default admin password!")
# Create initial API key for the admin user
from app.core.security import APIKeyManager
plain_key, hashed_key, prefix = APIKeyManager.generate_api_key()
admin_api_key = APIKey(
user_id=admin_user.id,
name="Default Admin API Key",
key_hash=hashed_key,
key_prefix=prefix,
is_active=True
)
admin_api_key.set_scopes(["admin"])
db.add(admin_api_key)
db.commit()
logging.info(f"Initial admin API key created: {plain_key}")
logging.warning("SECURITY: Save this API key securely, it won't be shown again!")
return True
except Exception as e:
logging.error(f"Failed to create initial user: {e}")
db.rollback()
return False
def initialize_database():
"""Initialize the complete database."""
logging.info("Starting database initialization...")
# Create tables
if not create_tables():
return False
# Create initial data
db = SessionLocal()
try:
# Create initial admin user
if not create_initial_user(db):
return False
logging.info("Database initialization completed successfully")
return True
except Exception as e:
logging.error(f"Database initialization failed: {e}")
return False
finally:
db.close()
if __name__ == "__main__":
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Initialize database
success = initialize_database()
if success:
print("✅ Database initialization completed successfully")
print("🔑 Default admin credentials: admin/admin123")
print("⚠️ SECURITY: Please change the default password immediately!")
else:
print("❌ Database initialization failed")
exit(1)

View File

@@ -0,0 +1,289 @@
"""
Security utilities for JWT token generation, validation, and API key management.
"""
import os
import uuid
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import jwt
from fastapi import HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Request
from sqlalchemy.orm import Session
# JWT Configuration
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-this-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "30"))
# Security scheme
security = HTTPBearer(auto_error=False)
class TokenManager:
"""Manages JWT token creation, validation, and refresh."""
@staticmethod
def create_access_token(
data: Dict[str, Any],
expires_delta: Optional[timedelta] = None
) -> str:
"""Create a JWT access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# Add standard claims
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "access",
"jti": str(uuid.uuid4()), # JWT ID for blacklisting
})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@staticmethod
def create_refresh_token(
user_id: int,
expires_delta: Optional[timedelta] = None
) -> str:
"""Create a JWT refresh token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode = {
"sub": str(user_id),
"exp": expire,
"iat": datetime.utcnow(),
"type": "refresh",
"jti": str(uuid.uuid4()),
}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@staticmethod
def verify_token(token: str) -> Dict[str, Any]:
"""Verify and decode a JWT token."""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
@staticmethod
def extract_user_id(token: str) -> int:
"""Extract user ID from a valid token."""
payload = TokenManager.verify_token(token)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token missing user information"
)
return int(user_id)
@staticmethod
def get_token_claims(token: str) -> Dict[str, Any]:
"""Get all claims from a token without verification (for expired tokens)."""
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_exp": False})
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token format"
)
class APIKeyManager:
"""Manages API key generation, validation, and permissions."""
@staticmethod
def generate_api_key() -> tuple[str, str, str]:
"""
Generate a new API key.
Returns: (plain_key, hashed_key, prefix)
"""
from app.models.auth import APIKey
plain_key, hashed_key = APIKey.generate_api_key()
prefix = plain_key[:8] # First 8 characters for identification
return plain_key, hashed_key, prefix
@staticmethod
def validate_api_key(session: Session, api_key: str) -> Optional[Dict[str, Any]]:
"""
Validate an API key and return user/key information.
Returns None if invalid.
"""
from app.models.auth import APIKey, User
# Find API key by trying to match the hash
api_keys = session.query(APIKey).filter(APIKey.is_active == True).all()
for key_record in api_keys:
if APIKey.verify_api_key(api_key, key_record.key_hash):
if not key_record.is_valid():
return None
# Get user information
user = session.query(User).filter(User.id == key_record.user_id).first()
if not user or not user.is_active:
return None
# Record usage
key_record.record_usage()
session.commit()
return {
"user_id": user.id,
"username": user.username,
"api_key_id": key_record.id,
"scopes": key_record.get_scopes(),
"is_superuser": user.is_superuser,
}
return None
@staticmethod
def check_scope_permission(user_scopes: List[str], required_scope: str) -> bool:
"""Check if user has required scope permission."""
# Admin users have all permissions
if "admin" in user_scopes:
return True
# Check for specific scope
if required_scope in user_scopes:
return True
# Check for wildcard permissions (e.g., "workflows:*" covers "workflows:read")
scope_parts = required_scope.split(":")
if len(scope_parts) >= 2:
wildcard_scope = f"{scope_parts[0]}:*"
if wildcard_scope in user_scopes:
return True
return False
class AuthenticationError(Exception):
"""Custom exception for authentication errors."""
def __init__(self, message: str, status_code: int = status.HTTP_401_UNAUTHORIZED):
self.message = message
self.status_code = status_code
super().__init__(self.message)
class AuthManager:
"""Main authentication manager combining JWT and API key auth."""
@staticmethod
def authenticate_request(
session: Session,
authorization: Optional[HTTPAuthorizationCredentials] = None,
api_key: Optional[str] = None
) -> Dict[str, Any]:
"""
Authenticate a request using either Bearer token or API key.
Returns user context information.
"""
# Try API key authentication first
if api_key:
user_context = APIKeyManager.validate_api_key(session, api_key)
if user_context:
user_context["auth_type"] = "api_key"
return user_context
else:
raise AuthenticationError("Invalid API key")
# Try JWT Bearer token authentication
if authorization and authorization.scheme.lower() == "bearer":
try:
payload = TokenManager.verify_token(authorization.credentials)
# Check if token is blacklisted
from app.models.auth import TokenBlacklist
jti = payload.get("jti")
if jti and TokenBlacklist.is_token_blacklisted(session, jti):
raise AuthenticationError("Token has been revoked")
# Get user information
user_id = int(payload.get("sub"))
from app.models.auth import User
user = session.query(User).filter(User.id == user_id).first()
if not user or not user.is_active:
raise AuthenticationError("User not found or inactive")
return {
"user_id": user.id,
"username": user.username,
"scopes": ["admin"] if user.is_superuser else [],
"is_superuser": user.is_superuser,
"auth_type": "jwt",
"token_jti": jti,
}
except HTTPException as e:
raise AuthenticationError(e.detail, e.status_code)
raise AuthenticationError("No valid authentication provided")
@staticmethod
def require_scope(required_scope: str):
"""Decorator to require specific scope for an endpoint."""
def decorator(func):
def wrapper(*args, **kwargs):
# This will be implemented in the dependency injection system
return func(*args, **kwargs)
return wrapper
return decorator
def create_token_response(user_id: int, user_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create a complete token response with access and refresh tokens."""
# Create access token with user data
access_token_data = {
"sub": str(user_id),
"username": user_data.get("username"),
"scopes": user_data.get("scopes", []),
}
access_token = TokenManager.create_access_token(access_token_data)
refresh_token = TokenManager.create_refresh_token(user_id)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60, # seconds
"user": user_data,
}
def get_password_hash(password: str) -> str:
"""Hash a password for storage."""
from app.models.auth import User
return User.hash_password(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash."""
from app.models.auth import pwd_context
return pwd_context.verify(plain_password, hashed_password)

View File

@@ -12,8 +12,7 @@ import socketio
from .core.hive_coordinator import HiveCoordinator from .core.hive_coordinator import HiveCoordinator
from .core.distributed_coordinator import DistributedCoordinator from .core.distributed_coordinator import DistributedCoordinator
from .core.database import engine, get_db, init_database_with_retry, test_database_connection from .core.database import engine, get_db, init_database_with_retry, test_database_connection
from .core.auth import get_current_user from .api import agents, workflows, executions, monitoring, projects, tasks, cluster, distributed_workflows, cli_agents, auth
from .api import agents, workflows, executions, monitoring, projects, tasks, cluster, distributed_workflows, cli_agents
# from .mcp.distributed_mcp_server import get_mcp_server # from .mcp.distributed_mcp_server import get_mcp_server
from .models.user import Base from .models.user import Base
from .models import agent, project # Import the new agent and project models from .models import agent, project # Import the new agent and project models
@@ -35,6 +34,11 @@ async def lifespan(app: FastAPI):
print("📊 Initializing database...") print("📊 Initializing database...")
init_database_with_retry() init_database_with_retry()
# Initialize auth database tables and initial data
print("🔐 Initializing authentication system...")
from .core.init_db import initialize_database
initialize_database()
# Test database connection # Test database connection
if not test_database_connection(): if not test_database_connection():
raise Exception("Database connection test failed") raise Exception("Database connection test failed")
@@ -100,6 +104,7 @@ app.add_middleware(
) )
# Include API routes # Include API routes
app.include_router(auth.router, prefix="/api/auth", tags=["authentication"])
app.include_router(agents.router, prefix="/api", tags=["agents"]) app.include_router(agents.router, prefix="/api", tags=["agents"])
app.include_router(workflows.router, prefix="/api", tags=["workflows"]) app.include_router(workflows.router, prefix="/api", tags=["workflows"])
app.include_router(executions.router, prefix="/api", tags=["executions"]) app.include_router(executions.router, prefix="/api", tags=["executions"])

297
backend/app/models/auth.py Normal file
View File

@@ -0,0 +1,297 @@
"""
Authentication and authorization models for Hive platform.
Includes users, API keys, and JWT token management.
"""
from datetime import datetime, timedelta
from typing import Optional, List
import secrets
import string
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from passlib.context import CryptContext
Base = declarative_base()
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(Base):
"""User model for authentication and authorization."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(255), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(255), nullable=True)
# User status and permissions
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
is_verified = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
last_login = Column(DateTime, nullable=True)
# Relationships
api_keys = relationship("APIKey", back_populates="user", cascade="all, delete-orphan")
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
def verify_password(self, password: str) -> bool:
"""Verify a password against the hashed password."""
return pwd_context.verify(password, self.hashed_password)
@classmethod
def hash_password(cls, password: str) -> str:
"""Hash a password for storage."""
return pwd_context.hash(password)
def set_password(self, password: str) -> None:
"""Set a new password for the user."""
self.hashed_password = self.hash_password(password)
def update_last_login(self) -> None:
"""Update the last login timestamp."""
self.last_login = datetime.utcnow()
def to_dict(self) -> dict:
"""Convert user to dictionary (excluding sensitive data)."""
return {
"id": self.id,
"username": self.username,
"email": self.email,
"full_name": self.full_name,
"is_active": self.is_active,
"is_superuser": self.is_superuser,
"is_verified": self.is_verified,
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_login": self.last_login.isoformat() if self.last_login else None,
}
class APIKey(Base):
"""API Key model for programmatic access to Hive API."""
__tablename__ = "api_keys"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# API Key details
name = Column(String(255), nullable=False) # Human-readable name
key_hash = Column(String(255), unique=True, index=True, nullable=False) # Hashed API key
key_prefix = Column(String(10), nullable=False) # First 8 chars for identification
# Permissions and scope
scopes = Column(Text, nullable=True) # JSON list of permissions
is_active = Column(Boolean, default=True)
# Usage tracking
last_used = Column(DateTime, nullable=True)
usage_count = Column(Integer, default=0)
# Expiration
expires_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="api_keys")
@classmethod
def generate_api_key(cls) -> tuple[str, str]:
"""
Generate a new API key.
Returns: (plain_key, hashed_key)
"""
# Generate a random API key: hive_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
alphabet = string.ascii_letters + string.digits
key_suffix = ''.join(secrets.choice(alphabet) for _ in range(32))
plain_key = f"hive_{key_suffix}"
# Hash the key for storage
hashed_key = pwd_context.hash(plain_key)
return plain_key, hashed_key
@classmethod
def verify_api_key(cls, plain_key: str, hashed_key: str) -> bool:
"""Verify an API key against the hashed version."""
return pwd_context.verify(plain_key, hashed_key)
def is_valid(self) -> bool:
"""Check if the API key is valid (active and not expired)."""
if not self.is_active:
return False
if self.expires_at and self.expires_at < datetime.utcnow():
return False
return True
def record_usage(self) -> None:
"""Record API key usage."""
self.last_used = datetime.utcnow()
self.usage_count += 1
def get_scopes(self) -> List[str]:
"""Get list of scopes/permissions for this API key."""
if not self.scopes:
return []
try:
import json
return json.loads(self.scopes)
except (json.JSONDecodeError, TypeError):
return []
def set_scopes(self, scopes: List[str]) -> None:
"""Set scopes/permissions for this API key."""
import json
self.scopes = json.dumps(scopes)
def to_dict(self) -> dict:
"""Convert API key to dictionary (excluding sensitive data)."""
return {
"id": self.id,
"name": self.name,
"key_prefix": self.key_prefix,
"scopes": self.get_scopes(),
"is_active": self.is_active,
"last_used": self.last_used.isoformat() if self.last_used else None,
"usage_count": self.usage_count,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class RefreshToken(Base):
"""Refresh token model for JWT token management."""
__tablename__ = "refresh_tokens"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Token details
token_hash = Column(String(255), unique=True, index=True, nullable=False)
jti = Column(String(36), unique=True, index=True, nullable=False) # JWT ID
# Token metadata
device_info = Column(String(512), nullable=True) # User agent, IP, etc.
is_active = Column(Boolean, default=True)
# Expiration
expires_at = Column(DateTime, nullable=False)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
last_used = Column(DateTime, default=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="refresh_tokens")
@classmethod
def generate_refresh_token(cls, length: int = 64) -> tuple[str, str]:
"""
Generate a new refresh token.
Returns: (plain_token, hashed_token)
"""
alphabet = string.ascii_letters + string.digits + "-_"
plain_token = ''.join(secrets.choice(alphabet) for _ in range(length))
hashed_token = pwd_context.hash(plain_token)
return plain_token, hashed_token
@classmethod
def verify_refresh_token(cls, plain_token: str, hashed_token: str) -> bool:
"""Verify a refresh token against the hashed version."""
return pwd_context.verify(plain_token, hashed_token)
def is_valid(self) -> bool:
"""Check if the refresh token is valid (active and not expired)."""
if not self.is_active:
return False
if self.expires_at < datetime.utcnow():
return False
return True
def revoke(self) -> None:
"""Revoke the refresh token."""
self.is_active = False
def record_usage(self) -> None:
"""Record refresh token usage."""
self.last_used = datetime.utcnow()
class TokenBlacklist(Base):
"""Blacklist for revoked JWT tokens."""
__tablename__ = "token_blacklist"
id = Column(Integer, primary_key=True, index=True)
jti = Column(String(36), unique=True, index=True, nullable=False) # JWT ID
token_type = Column(String(20), nullable=False) # "access" or "refresh"
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
@classmethod
def is_token_blacklisted(cls, session, jti: str) -> bool:
"""Check if a token is blacklisted."""
token = session.query(cls).filter(cls.jti == jti).first()
return token is not None
@classmethod
def blacklist_token(cls, session, jti: str, token_type: str, expires_at: datetime) -> None:
"""Add a token to the blacklist."""
blacklisted_token = cls(
jti=jti,
token_type=token_type,
expires_at=expires_at
)
session.add(blacklisted_token)
session.commit()
@classmethod
def cleanup_expired_tokens(cls, session) -> int:
"""Remove expired tokens from blacklist and return count removed."""
now = datetime.utcnow()
expired_tokens = session.query(cls).filter(cls.expires_at < now)
count = expired_tokens.count()
expired_tokens.delete()
session.commit()
return count
# Available scopes for API keys
API_SCOPES = {
"agents:read": "View agent information and status",
"agents:write": "Manage agents (start, stop, configure)",
"workflows:read": "View workflow information and executions",
"workflows:write": "Create, modify, and execute workflows",
"tasks:read": "View task information and results",
"tasks:write": "Create and manage tasks",
"metrics:read": "View system metrics and performance data",
"system:read": "View system status and configuration",
"system:write": "Modify system configuration",
"admin": "Full administrative access",
}
# Default scopes for new API keys
DEFAULT_API_SCOPES = [
"agents:read",
"workflows:read",
"tasks:read",
"metrics:read",
"system:read"
]

View File

@@ -0,0 +1,455 @@
/**
* API Key Management Component
* Provides interface for creating, viewing, and managing API keys
*/
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import {
Key,
Plus,
Copy,
Eye,
EyeOff,
Trash2,
CheckCircle,
AlertCircle,
Calendar,
Shield
} from 'lucide-react';
import { useAuthenticatedFetch } from '../../contexts/AuthContext';
interface APIKey {
id: number;
name: string;
key_prefix: string;
scopes: string[];
is_active: boolean;
last_used?: string;
created_at: string;
expires_at?: string;
}
interface CreateAPIKeyRequest {
name: string;
scopes: string[];
expires_days?: number;
}
interface CreateAPIKeyResponse {
api_key: APIKey;
plain_key: string;
}
const API_SCOPES = [
{ id: 'read', label: 'Read Access', description: 'View data and resources' },
{ id: 'write', label: 'Write Access', description: 'Create and modify resources' },
{ id: 'admin', label: 'Admin Access', description: 'Full administrative access' },
{ id: 'agents', label: 'Agent Management', description: 'Manage AI agents' },
{ id: 'workflows', label: 'Workflow Management', description: 'Create and run workflows' },
{ id: 'monitoring', label: 'Monitoring', description: 'Access monitoring and metrics' }
];
export const APIKeyManager: React.FC = () => {
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [newKey, setNewKey] = useState<string | null>(null);
const [showNewKey, setShowNewKey] = useState(false);
const [copiedKeyId, setCopiedKeyId] = useState<number | null>(null);
// Create form state
const [createForm, setCreateForm] = useState({
name: '',
scopes: [] as string[],
expires_days: undefined as number | undefined
});
const authenticatedFetch = useAuthenticatedFetch();
useEffect(() => {
loadAPIKeys();
}, []);
const loadAPIKeys = async () => {
try {
setIsLoading(true);
const response = await authenticatedFetch('/api/auth/api-keys');
if (response.ok) {
const keys = await response.json();
setApiKeys(keys);
} else {
setError('Failed to load API keys');
}
} catch (err: any) {
setError(err.message || 'Failed to load API keys');
} finally {
setIsLoading(false);
}
};
const handleCreateAPIKey = async () => {
try {
const requestData: CreateAPIKeyRequest = {
name: createForm.name,
scopes: createForm.scopes,
...(createForm.expires_days && { expires_days: createForm.expires_days })
};
const response = await authenticatedFetch('/api/auth/api-keys', {
method: 'POST',
body: JSON.stringify(requestData)
});
if (response.ok) {
const data: CreateAPIKeyResponse = await response.json();
setNewKey(data.plain_key);
setApiKeys(prev => [...prev, data.api_key]);
// Reset form
setCreateForm({
name: '',
scopes: [],
expires_days: undefined
});
setError('');
} else {
const errorData = await response.json();
setError(errorData.detail || 'Failed to create API key');
}
} catch (err: any) {
setError(err.message || 'Failed to create API key');
}
};
const handleDeleteAPIKey = async (keyId: number, keyName: string) => {
if (!confirm(`Are you sure you want to delete the API key "${keyName}"? This action cannot be undone.`)) {
return;
}
try {
const response = await authenticatedFetch(`/api/auth/api-keys/${keyId}`, {
method: 'DELETE'
});
if (response.ok) {
setApiKeys(prev => prev.filter(key => key.id !== keyId));
} else {
setError('Failed to delete API key');
}
} catch (err: any) {
setError(err.message || 'Failed to delete API key');
}
};
const handleToggleAPIKey = async (keyId: number, isActive: boolean) => {
try {
const response = await authenticatedFetch(`/api/auth/api-keys/${keyId}/toggle`, {
method: 'PATCH'
});
if (response.ok) {
const updatedKey = await response.json();
setApiKeys(prev => prev.map(key => key.id === keyId ? updatedKey : key));
} else {
setError('Failed to update API key status');
}
} catch (err: any) {
setError(err.message || 'Failed to update API key status');
}
};
const copyToClipboard = (text: string, keyId?: number) => {
navigator.clipboard.writeText(text);
if (keyId) {
setCopiedKeyId(keyId);
setTimeout(() => setCopiedKeyId(null), 2000);
}
};
const handleScopeChange = (scope: string, checked: boolean) => {
setCreateForm(prev => ({
...prev,
scopes: checked
? [...prev.scopes, scope]
: prev.scopes.filter(s => s !== scope)
}));
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">API Keys</h2>
<p className="text-gray-600 mt-1">
Manage API keys for programmatic access to Hive
</p>
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
Create API Key
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Create New API Key</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="key-name">Key Name</Label>
<Input
id="key-name"
value={createForm.name}
onChange={(e) => setCreateForm(prev => ({ ...prev, name: e.target.value }))}
placeholder="e.g., Production API Key"
className="mt-1"
/>
</div>
<div>
<Label>Scopes</Label>
<div className="mt-2 space-y-2 max-h-48 overflow-y-auto">
{API_SCOPES.map(scope => (
<div key={scope.id} className="flex items-start space-x-3">
<Checkbox
id={scope.id}
checked={createForm.scopes.includes(scope.id)}
onCheckedChange={(checked) => handleScopeChange(scope.id, !!checked)}
/>
<div>
<Label htmlFor={scope.id} className="text-sm font-medium">
{scope.label}
</Label>
<p className="text-xs text-gray-500">{scope.description}</p>
</div>
</div>
))}
</div>
</div>
<div>
<Label htmlFor="expires-days">Expiration (days)</Label>
<Select
value={createForm.expires_days?.toString() || ''}
onValueChange={(value) => setCreateForm(prev => ({
...prev,
expires_days: value ? parseInt(value) : undefined
}))}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Never expires" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">Never expires</SelectItem>
<SelectItem value="7">7 days</SelectItem>
<SelectItem value="30">30 days</SelectItem>
<SelectItem value="90">90 days</SelectItem>
<SelectItem value="365">1 year</SelectItem>
</SelectContent>
</Select>
</div>
<Button
onClick={handleCreateAPIKey}
disabled={!createForm.name || createForm.scopes.length === 0}
className="w-full"
>
Create API Key
</Button>
</div>
</DialogContent>
</Dialog>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{newKey && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
<div className="space-y-2">
<p className="font-medium">API Key Created Successfully!</p>
<p className="text-sm">
Please copy your API key now. You won't be able to see it again.
</p>
<div className="flex items-center space-x-2 bg-gray-50 p-2 rounded">
<code className="flex-1 text-sm">
{showNewKey ? newKey : ''.repeat(40)}
</code>
<Button
variant="ghost"
size="sm"
onClick={() => setShowNewKey(!showNewKey)}
>
{showNewKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(newKey)}
>
<Copy className="w-4 h-4" />
</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setNewKey(null)}
>
I've saved the key
</Button>
</div>
</AlertDescription>
</Alert>
)}
<div className="space-y-4">
{apiKeys.length === 0 ? (
<Card>
<CardContent className="text-center py-8">
<Key className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No API Keys</h3>
<p className="text-gray-600 mb-4">
Create your first API key to start using the Hive API programmatically.
</p>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Create API Key
</Button>
</CardContent>
</Card>
) : (
apiKeys.map(apiKey => (
<Card key={apiKey.id}>
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-medium text-gray-900">
{apiKey.name}
</h3>
<Badge variant={apiKey.is_active ? "default" : "secondary"}>
{apiKey.is_active ? "Active" : "Disabled"}
</Badge>
</div>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<Key className="w-4 h-4" />
<span>Key: {apiKey.key_prefix}...</span>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(`${apiKey.key_prefix}...`)}
className="h-auto p-1"
>
{copiedKeyId === apiKey.id ? (
<CheckCircle className="w-3 h-3 text-green-600" />
) : (
<Copy className="w-3 h-3" />
)}
</Button>
</div>
<div className="flex items-center space-x-1">
<Calendar className="w-4 h-4" />
<span>Created: {formatDate(apiKey.created_at)}</span>
</div>
{apiKey.last_used && (
<div className="flex items-center space-x-1">
<span>Last used: {formatDate(apiKey.last_used)}</span>
</div>
)}
</div>
{apiKey.expires_at && (
<div className="flex items-center space-x-1 text-amber-600">
<Calendar className="w-4 h-4" />
<span>Expires: {formatDate(apiKey.expires_at)}</span>
</div>
)}
<div className="flex items-center space-x-1">
<Shield className="w-4 h-4" />
<span>Scopes:</span>
<div className="flex space-x-1">
{apiKey.scopes.map(scope => (
<Badge key={scope} variant="outline" className="text-xs">
{scope}
</Badge>
))}
</div>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleToggleAPIKey(apiKey.id, !apiKey.is_active)}
>
{apiKey.is_active ? 'Disable' : 'Enable'}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteAPIKey(apiKey.id, apiKey.name)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
</div>
);
};
export default APIKeyManager;

View File

@@ -0,0 +1,303 @@
/**
* Authentication Dashboard Component
* Main dashboard for authentication and authorization management
*/
import React, { useState } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
User,
Key,
Shield,
Clock,
Settings,
LogOut,
Calendar,
Activity
} from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import { APIKeyManager } from './APIKeyManager';
export const AuthDashboard: React.FC = () => {
const { user, tokens, logout } = useAuth();
const [activeTab, setActiveTab] = useState('profile');
const handleLogout = async () => {
await logout();
};
const formatDate = (dateString?: string) => {
if (!dateString) return 'Never';
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getTokenExpirationTime = () => {
if (!tokens?.access_token) return null;
try {
const payload = JSON.parse(atob(tokens.access_token.split('.')[1]));
return new Date(payload.exp * 1000);
} catch {
return null;
}
};
const tokenExpiration = getTokenExpirationTime();
const isTokenExpiringSoon = tokenExpiration && (tokenExpiration.getTime() - Date.now()) < 5 * 60 * 1000; // 5 minutes
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-6xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Authentication Dashboard</h1>
<p className="text-gray-600 mt-1">
Manage your account, tokens, and API keys
</p>
</div>
<Button onClick={handleLogout} variant="outline">
<LogOut className="w-4 h-4 mr-2" />
Sign Out
</Button>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-blue-100 rounded-lg">
<User className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-600">Account Status</p>
<p className="font-medium">
{user?.is_active ? 'Active' : 'Inactive'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-green-100 rounded-lg">
<Shield className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-600">Role</p>
<p className="font-medium">
{user?.is_superuser ? 'Administrator' : 'User'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${isTokenExpiringSoon ? 'bg-red-100' : 'bg-yellow-100'}`}>
<Clock className={`w-5 h-5 ${isTokenExpiringSoon ? 'text-red-600' : 'text-yellow-600'}`} />
</div>
<div>
<p className="text-sm text-gray-600">Token Expires</p>
<p className="font-medium text-xs">
{tokenExpiration ? formatDate(tokenExpiration.toISOString()) : 'Unknown'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Activity className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-sm text-gray-600">Last Login</p>
<p className="font-medium text-xs">
{formatDate(user?.last_login)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Main Content */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="api-keys">API Keys</TabsTrigger>
<TabsTrigger value="tokens">Tokens</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<User className="w-5 h-5" />
<span>User Profile</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-gray-700">Username</label>
<p className="mt-1 text-gray-900">{user?.username}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Email</label>
<p className="mt-1 text-gray-900">{user?.email}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Full Name</label>
<p className="mt-1 text-gray-900">{user?.full_name || 'Not set'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Account Created</label>
<p className="mt-1 text-gray-900">{formatDate(user?.created_at)}</p>
</div>
</div>
<div className="flex flex-wrap gap-2 pt-4">
<Badge variant={user?.is_active ? "default" : "secondary"}>
{user?.is_active ? "Active" : "Inactive"}
</Badge>
<Badge variant={user?.is_verified ? "default" : "secondary"}>
{user?.is_verified ? "Verified" : "Unverified"}
</Badge>
{user?.is_superuser && (
<Badge variant="destructive">
Administrator
</Badge>
)}
</div>
<div className="pt-4 border-t">
<Button variant="outline">
<Settings className="w-4 h-4 mr-2" />
Edit Profile
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="api-keys">
<APIKeyManager />
</TabsContent>
<TabsContent value="tokens" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Key className="w-5 h-5" />
<span>Authentication Tokens</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">Access Token</h4>
<Badge variant={isTokenExpiringSoon ? "destructive" : "default"}>
{isTokenExpiringSoon ? "Expiring Soon" : "Active"}
</Badge>
</div>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center space-x-2">
<Calendar className="w-4 h-4" />
<span>
Expires: {tokenExpiration ? formatDate(tokenExpiration.toISOString()) : 'Unknown'}
</span>
</div>
<div className="flex items-center space-x-2">
<Key className="w-4 h-4" />
<span>Type: Bearer Token</span>
</div>
</div>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">Refresh Token</h4>
<Badge variant="secondary">
Available
</Badge>
</div>
<div className="space-y-2 text-sm text-gray-600">
<p>Used to automatically refresh access tokens when they expire.</p>
</div>
</div>
</div>
<div className="pt-4 border-t">
<div className="flex space-x-2">
<Button variant="outline">
Refresh Token
</Button>
<Button variant="outline" onClick={handleLogout}>
Revoke All Tokens
</Button>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Token Usage Guidelines</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3 text-sm">
<div>
<h5 className="font-medium">Bearer Token Authentication</h5>
<p className="text-gray-600">
Include your access token in API requests using the Authorization header:
</p>
<code className="block mt-1 p-2 bg-gray-100 rounded text-xs">
Authorization: Bearer YOUR_ACCESS_TOKEN
</code>
</div>
<div>
<h5 className="font-medium">Token Security</h5>
<ul className="list-disc list-inside text-gray-600 space-y-1">
<li>Never share your tokens with others</li>
<li>Use HTTPS for all API requests</li>
<li>Store tokens securely in your applications</li>
<li>Revoke tokens if they may be compromised</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default AuthDashboard;

View File

@@ -0,0 +1,178 @@
/**
* Login Form Component
* Provides user authentication interface with JWT token support
*/
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Eye, EyeOff, Lock, User, AlertCircle } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
interface LoginFormProps {
onSuccess?: () => void;
redirectTo?: string;
}
export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess, redirectTo = '/dashboard' }) => {
const [formData, setFormData] = useState({
username: '',
password: '',
});
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
// Clear error when user starts typing
if (error) setError('');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
await login(formData.username, formData.password);
if (onSuccess) {
onSuccess();
} else {
navigate(redirectTo);
}
} catch (err: any) {
setError(err.message || 'Login failed. Please check your credentials.');
} finally {
setIsLoading(false);
}
};
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto mb-4 w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center">
<Lock className="w-6 h-6 text-white" />
</div>
<CardTitle className="text-2xl font-bold text-gray-900">
Welcome to Hive
</CardTitle>
<p className="text-gray-600 mt-2">
Sign in to your account to continue
</p>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleInputChange}
placeholder="Enter your username"
className="pl-10"
required
disabled={isLoading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={handleInputChange}
placeholder="Enter your password"
className="pl-10 pr-10"
required
disabled={isLoading}
/>
<button
type="button"
onClick={togglePasswordVisibility}
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
disabled={isLoading}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<Button
type="submit"
className="w-full"
disabled={isLoading || !formData.username || !formData.password}
>
{isLoading ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="mt-6 text-center text-sm text-gray-600">
<p>
Need help?{' '}
<Link
to="/docs"
className="text-blue-600 hover:text-blue-500 font-medium"
>
View documentation
</Link>
</p>
</div>
{/* Development info */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-xs text-yellow-800">
<strong>Development Mode:</strong><br />
Default credentials: admin / admin123
</p>
</div>
)}
</CardContent>
</Card>
</div>
);
};
export default LoginForm;

View File

@@ -1,105 +1,229 @@
import React, { createContext, useContext, useState, useEffect } from 'react'; /**
* Authentication Context
* Manages user authentication state, JWT tokens, and API key authentication
*/
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User { interface User {
id: string; id: number;
username: string; username: string;
name: string; email: string;
role: string; full_name?: string;
email?: string; is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
created_at: string;
last_login?: string;
}
interface AuthTokens {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
} }
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
tokens: AuthTokens | null;
isAuthenticated: boolean; isAuthenticated: boolean;
isLoading: boolean; isLoading: boolean;
login: (username: string, password: string) => Promise<boolean>; login: (username: string, password: string) => Promise<void>;
logout: () => void; logout: () => void;
token: string | null; refreshToken: () => Promise<boolean>;
updateUser: (userData: Partial<User>) => void;
} }
const AuthContext = createContext<AuthContextType | null>(null); const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps { interface AuthProviderProps {
children: React.ReactNode; children: ReactNode;
} }
const API_BASE_URL = process.env.REACT_APP_API_URL || '/api';
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null); const [tokens, setTokens] = useState<AuthTokens | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Check for existing authentication on mount const isAuthenticated = !!user && !!tokens;
useEffect(() => {
const checkAuth = () => {
const storedToken = localStorage.getItem('auth_token');
const storedUser = localStorage.getItem('user');
if (storedToken && storedUser) { // Initialize auth state from localStorage
try { useEffect(() => {
const parsedUser = JSON.parse(storedUser); const initializeAuth = async () => {
setToken(storedToken); try {
setUser(parsedUser); const storedTokens = localStorage.getItem('hive_tokens');
} catch (error) { const storedUser = localStorage.getItem('hive_user');
console.error('Failed to parse stored user data:', error);
localStorage.removeItem('auth_token'); if (storedTokens && storedUser) {
localStorage.removeItem('user'); const parsedTokens: AuthTokens = JSON.parse(storedTokens);
const parsedUser: User = JSON.parse(storedUser);
// Check if tokens are still valid
if (await validateTokens(parsedTokens)) {
setTokens(parsedTokens);
setUser(parsedUser);
} else {
// Try to refresh tokens
const refreshed = await refreshTokenWithStoredData(parsedTokens);
if (!refreshed) {
clearAuthData();
}
}
} }
} catch (error) {
console.error('Error initializing auth:', error);
clearAuthData();
} finally {
setIsLoading(false);
} }
setIsLoading(false);
}; };
checkAuth(); initializeAuth();
}, []); }, []);
const login = async (username: string, password: string): Promise<boolean> => { const validateTokens = async (tokens: AuthTokens): Promise<boolean> => {
try { try {
// In a real application, this would make an API call const response = await fetch(`${API_BASE_URL}/auth/me`, {
// For demo purposes, we'll simulate authentication headers: {
if (username === 'admin' && password === 'hiveadmin') { 'Authorization': `Bearer ${tokens.access_token}`,
const mockToken = 'mock-jwt-token-' + Date.now(); },
const mockUser: User = { });
id: '1', return response.ok;
username: 'admin', } catch {
name: 'System Administrator',
role: 'administrator',
email: 'admin@hive.local'
};
setToken(mockToken);
setUser(mockUser);
localStorage.setItem('auth_token', mockToken);
localStorage.setItem('user', JSON.stringify(mockUser));
return true;
}
return false;
} catch (error) {
console.error('Login failed:', error);
return false; return false;
} }
}; };
const logout = () => { const refreshTokenWithStoredData = async (oldTokens: AuthTokens): Promise<boolean> => {
setUser(null); try {
setToken(null); const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
localStorage.removeItem('auth_token'); method: 'POST',
localStorage.removeItem('user'); headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: oldTokens.refresh_token,
}),
});
if (response.ok) {
const data = await response.json();
const newTokens: AuthTokens = {
access_token: data.access_token,
refresh_token: data.refresh_token,
token_type: data.token_type,
expires_in: data.expires_in,
};
setTokens(newTokens);
setUser(data.user);
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
localStorage.setItem('hive_user', JSON.stringify(data.user));
return true;
} else {
return false;
}
} catch (error) {
console.error('Token refresh failed:', error);
return false;
}
}; };
const value: AuthContextType = { const login = async (username: string, password: string): Promise<void> => {
try {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Login failed');
}
const data = await response.json();
const newTokens: AuthTokens = {
access_token: data.access_token,
refresh_token: data.refresh_token,
token_type: data.token_type,
expires_in: data.expires_in,
};
setTokens(newTokens);
setUser(data.user);
// Store in localStorage
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
localStorage.setItem('hive_user', JSON.stringify(data.user));
} catch (error: any) {
throw new Error(error.message || 'Login failed');
}
};
const logout = async (): Promise<void> => {
try {
// Call logout endpoint if we have a token
if (tokens) {
await fetch(`${API_BASE_URL}/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${tokens.access_token}`,
},
});
}
} catch (error) {
console.error('Logout API call failed:', error);
} finally {
clearAuthData();
}
};
const refreshToken = async (): Promise<boolean> => {
if (!tokens?.refresh_token) {
return false;
}
return await refreshTokenWithStoredData(tokens);
};
const updateUser = (userData: Partial<User>): void => {
if (user) {
const updatedUser = { ...user, ...userData };
setUser(updatedUser);
localStorage.setItem('hive_user', JSON.stringify(updatedUser));
}
};
const clearAuthData = (): void => {
setUser(null);
setTokens(null);
localStorage.removeItem('hive_tokens');
localStorage.removeItem('hive_user');
};
const contextValue: AuthContextType = {
user, user,
isAuthenticated: !!user && !!token, tokens,
isAuthenticated,
isLoading, isLoading,
login, login,
logout, logout,
token refreshToken,
updateUser,
}; };
return ( return (
<AuthContext.Provider value={value}> <AuthContext.Provider value={contextValue}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
@@ -107,19 +231,55 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
export const useAuth = (): AuthContextType => { export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (!context) { if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider'); throw new Error('useAuth must be used within an AuthProvider');
} }
return context; return context;
}; };
// Helper hook for protected routes // Hook for making authenticated API requests
export const useRequireAuth = () => { export const useAuthenticatedFetch = () => {
const { isAuthenticated, isLoading } = useAuth(); const { tokens, refreshToken, logout } = useAuth();
return { const authenticatedFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
isAuthenticated, if (!tokens) {
isLoading, throw new Error('No authentication tokens available');
shouldRedirect: !isLoading && !isAuthenticated }
const headers = {
'Content-Type': 'application/json',
...options.headers,
'Authorization': `Bearer ${tokens.access_token}`,
};
let response = await fetch(url, {
...options,
headers,
});
// If token expired, try to refresh and retry
if (response.status === 401) {
const refreshed = await refreshToken();
if (refreshed) {
// Retry with new token
response = await fetch(url, {
...options,
headers: {
...headers,
'Authorization': `Bearer ${tokens.access_token}`,
},
});
} else {
// Refresh failed, logout user
logout();
throw new Error('Authentication expired');
}
}
return response;
}; };
};
return authenticatedFetch;
};
export default AuthContext;