first commit

This commit is contained in:
rafaeldpsilva
2025-09-09 13:46:42 +01:00
commit a7a18e6295
77 changed files with 8678 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
FROM python:3.9-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 8001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8001/health || exit 1
# Run the application
CMD ["python", "main.py"]

View File

@@ -0,0 +1,65 @@
"""
Database connection for Token Service
"""
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
import logging
import os
logger = logging.getLogger(__name__)
# Database configuration
MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27017")
DATABASE_NAME = os.getenv("DATABASE_NAME", "energy_dashboard_tokens")
# Global database client
_client: AsyncIOMotorClient = None
_database: AsyncIOMotorDatabase = None
async def connect_to_mongo():
"""Create database connection"""
global _client, _database
try:
_client = AsyncIOMotorClient(MONGO_URL)
_database = _client[DATABASE_NAME]
# Test connection
await _database.command("ping")
logger.info(f"Connected to MongoDB: {DATABASE_NAME}")
# Create indexes for performance
await create_indexes()
except Exception as e:
logger.error(f"Failed to connect to MongoDB: {e}")
raise
async def close_mongo_connection():
"""Close database connection"""
global _client
if _client:
_client.close()
logger.info("Disconnected from MongoDB")
async def get_database() -> AsyncIOMotorDatabase:
"""Get database instance"""
global _database
if _database is None:
raise RuntimeError("Database not initialized. Call connect_to_mongo() first.")
return _database
async def create_indexes():
"""Create database indexes for performance"""
db = await get_database()
# Indexes for tokens collection
await db.tokens.create_index("token", unique=True)
await db.tokens.create_index("active")
await db.tokens.create_index("expires_at")
await db.tokens.create_index("name")
logger.info("Database indexes created")

View File

@@ -0,0 +1,190 @@
"""
Token Management Microservice
Handles JWT authentication, token generation, validation, and resource access control.
Port: 8001
"""
import asyncio
from datetime import datetime
from fastapi import FastAPI, HTTPException, Depends, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import logging
from typing import List, Optional
from models import (
TokenGenerateRequest, TokenResponse, TokenValidationResponse,
TokenListResponse, HealthResponse
)
from database import connect_to_mongo, close_mongo_connection, get_database
from token_service import TokenService
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
security = HTTPBearer()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager"""
logger.info("Token Service starting up...")
await connect_to_mongo()
logger.info("Token Service startup complete")
yield
logger.info("Token Service shutting down...")
await close_mongo_connection()
logger.info("Token Service shutdown complete")
app = FastAPI(
title="Token Management Service",
description="JWT authentication and token management microservice",
version="1.0.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Dependency for database
async def get_db():
return await get_database()
@app.get("/health", response_model=HealthResponse)
async def health_check():
"""Health check endpoint"""
try:
db = await get_database()
await db.command("ping")
return HealthResponse(
service="token-service",
status="healthy",
timestamp=datetime.utcnow(),
version="1.0.0"
)
except Exception as e:
logger.error(f"Health check failed: {e}")
raise HTTPException(status_code=503, detail="Service Unavailable")
@app.get("/tokens", response_model=TokenListResponse)
async def get_tokens(db=Depends(get_db)):
"""Get all tokens"""
try:
token_service = TokenService(db)
tokens = await token_service.get_tokens()
return TokenListResponse(
tokens=tokens,
count=len(tokens)
)
except Exception as e:
logger.error(f"Error getting tokens: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.post("/tokens/generate", response_model=TokenResponse)
async def generate_token(request: TokenGenerateRequest, db=Depends(get_db)):
"""Generate a new JWT token"""
try:
token_service = TokenService(db)
token = token_service.generate_token(
name=request.name,
list_of_resources=request.list_of_resources,
data_aggregation=request.data_aggregation,
time_aggregation=request.time_aggregation,
embargo=request.embargo,
exp_hours=request.exp_hours
)
return TokenResponse(token=token)
except Exception as e:
logger.error(f"Error generating token: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.post("/tokens/validate", response_model=TokenValidationResponse)
async def validate_token(token: str, db=Depends(get_db)):
"""Validate and decode a JWT token"""
try:
token_service = TokenService(db)
is_valid = await token_service.is_token_valid(token)
decoded = token_service.decode_token(token) if is_valid else None
return TokenValidationResponse(
valid=is_valid,
token=token,
decoded=decoded if is_valid and "error" not in (decoded or {}) else None,
error=decoded.get("error") if decoded and "error" in decoded else None
)
except Exception as e:
logger.error(f"Error validating token: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.post("/tokens/save")
async def save_token(token: str, db=Depends(get_db)):
"""Save a token to database"""
try:
token_service = TokenService(db)
result = await token_service.insert_token(token)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error saving token: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.post("/tokens/revoke")
async def revoke_token(token: str, db=Depends(get_db)):
"""Revoke a token"""
try:
token_service = TokenService(db)
result = await token_service.revoke_token(token)
return result
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Error revoking token: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.get("/tokens/{token}/permissions")
async def get_token_permissions(token: str, db=Depends(get_db)):
"""Get permissions for a specific token"""
try:
token_service = TokenService(db)
permissions = await token_service.get_token_permissions(token)
if permissions:
return {"permissions": permissions}
else:
raise HTTPException(status_code=401, detail="Invalid or expired token")
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting token permissions: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.delete("/tokens/cleanup")
async def cleanup_expired_tokens(db=Depends(get_db)):
"""Clean up expired tokens"""
try:
token_service = TokenService(db)
expired_count = await token_service.cleanup_expired_tokens()
return {
"message": "Expired tokens cleaned up",
"expired_tokens_removed": expired_count
}
except Exception as e:
logger.error(f"Error cleaning up tokens: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)

View File

@@ -0,0 +1,55 @@
"""
Pydantic models for Token Management Service
"""
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
class TokenGenerateRequest(BaseModel):
"""Request model for token generation"""
name: str = Field(..., description="Token owner name")
list_of_resources: List[str] = Field(..., description="List of accessible resources")
data_aggregation: bool = Field(default=False, description="Allow data aggregation")
time_aggregation: bool = Field(default=False, description="Allow time aggregation")
embargo: int = Field(default=0, description="Embargo period in seconds")
exp_hours: int = Field(default=24, description="Token expiration in hours")
class TokenResponse(BaseModel):
"""Response model for token operations"""
token: str = Field(..., description="JWT token")
class TokenValidationResponse(BaseModel):
"""Response model for token validation"""
valid: bool = Field(..., description="Whether token is valid")
token: str = Field(..., description="Original token")
decoded: Optional[Dict[str, Any]] = Field(None, description="Decoded token payload")
error: Optional[str] = Field(None, description="Error message if invalid")
class TokenRecord(BaseModel):
"""Token database record model"""
token: str
datetime: str
active: bool
name: str
resources: List[str]
expires_at: str
created_at: str
updated_at: str
class TokenListResponse(BaseModel):
"""Response model for token list"""
tokens: List[Dict[str, Any]]
count: int
class HealthResponse(BaseModel):
"""Health check response"""
service: str
status: str
timestamp: datetime
version: str
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}

View File

@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
pymongo
motor
PyJWT
python-dotenv
pydantic

View File

@@ -0,0 +1,157 @@
"""
Token service implementation
"""
import jwt
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from motor.motor_asyncio import AsyncIOMotorDatabase
import os
class TokenService:
"""Service for managing JWT tokens and authentication"""
def __init__(self, db: AsyncIOMotorDatabase, secret_key: str = None):
self.db = db
self.secret_key = secret_key or os.getenv("JWT_SECRET_KEY", "energy-dashboard-secret-key")
self.tokens_collection = db.tokens
def generate_token(self, name: str, list_of_resources: List[str],
data_aggregation: bool = False, time_aggregation: bool = False,
embargo: int = 0, exp_hours: int = 24) -> str:
"""Generate a new JWT token with specified permissions"""
# Calculate expiration time
exp_timestamp = int((datetime.utcnow() + timedelta(hours=exp_hours)).timestamp())
# Create token payload
payload = {
"name": name,
"list_of_resources": list_of_resources,
"data_aggregation": data_aggregation,
"time_aggregation": time_aggregation,
"embargo": embargo,
"exp": exp_timestamp,
"iat": int(datetime.utcnow().timestamp()),
"jti": str(uuid.uuid4()) # unique token ID
}
# Generate JWT token
token = jwt.encode(payload, self.secret_key, algorithm="HS256")
return token
def decode_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Decode and verify JWT token"""
try:
payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
return {"error": "Token has expired"}
except jwt.InvalidTokenError:
return {"error": "Invalid token"}
async def insert_token(self, token: str) -> Dict[str, Any]:
"""Save token to database"""
now = datetime.utcnow()
# Decode token to verify it's valid
decoded = self.decode_token(token)
if decoded and "error" not in decoded:
token_record = {
"token": token,
"datetime": now,
"active": True,
"created_at": now,
"updated_at": now,
"name": decoded.get("name", ""),
"resources": decoded.get("list_of_resources", []),
"expires_at": datetime.fromtimestamp(decoded.get("exp", 0))
}
# Upsert token (update if exists, insert if not)
await self.tokens_collection.replace_one(
{"token": token},
token_record,
upsert=True
)
return {
"token": token,
"datetime": now.isoformat(),
"active": True
}
else:
raise ValueError("Invalid token cannot be saved")
async def revoke_token(self, token: str) -> Dict[str, Any]:
"""Revoke a token by marking it as inactive"""
now = datetime.utcnow()
result = await self.tokens_collection.update_one(
{"token": token},
{
"$set": {
"active": False,
"updated_at": now,
"revoked_at": now
}
}
)
if result.matched_count > 0:
return {
"token": token,
"datetime": now.isoformat(),
"active": False
}
else:
raise ValueError("Token not found")
async def get_tokens(self) -> List[Dict[str, Any]]:
"""Get all tokens from database"""
cursor = self.tokens_collection.find({})
tokens = []
async for token_record in cursor:
# Convert ObjectId to string and datetime to ISO format
token_record["_id"] = str(token_record["_id"])
for field in ["datetime", "created_at", "updated_at", "expires_at", "revoked_at"]:
if field in token_record and token_record[field]:
token_record[field] = token_record[field].isoformat()
tokens.append(token_record)
return tokens
async def is_token_valid(self, token: str) -> bool:
"""Check if token is valid and active"""
# Check if token exists and is active in database
token_record = await self.tokens_collection.find_one({
"token": token,
"active": True
})
if not token_record:
return False
# Verify JWT signature and expiration
decoded = self.decode_token(token)
return decoded is not None and "error" not in decoded
async def get_token_permissions(self, token: str) -> Optional[Dict[str, Any]]:
"""Get permissions for a valid token"""
if await self.is_token_valid(token):
return self.decode_token(token)
return None
async def cleanup_expired_tokens(self) -> int:
"""Remove expired tokens from database"""
now = datetime.utcnow()
# Delete tokens that have expired
result = await self.tokens_collection.delete_many({
"expires_at": {"$lt": now}
})
return result.deleted_count