Skip to content

Commit ec59758

Browse files
Merge pull request #9 from ISCODEVUTB/feat/api-integration-and-docs
Feat/api integration and docs
2 parents 650091b + c481c01 commit ec59758

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+5483
-1023
lines changed

backend/api/api_gateway/main.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
# Load environment variables
1616
load_dotenv()
1717

18+
MAX_REQUEST_BODY_SIZE = 1 * 1024 * 1024 # 1MB
19+
EXCLUDED_HEADERS = {
20+
"host", "connection", "keep-alive", "proxy-authenticate",
21+
"proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade",
22+
"content-length",
23+
}
24+
1825
# Create FastAPI app
1926
app = FastAPI(
2027
title="TaskHub API Gateway",
@@ -89,24 +96,38 @@ async def forward_request(
8996
Returns:
9097
JSONResponse: Response from service
9198
"""
92-
# Get request body
93-
body = await request.body()
94-
95-
# Get request headers
96-
headers = dict(request.headers)
99+
# Filter headers
100+
temp_headers = {}
101+
for name, value in request.headers.items():
102+
if name.lower() not in EXCLUDED_HEADERS:
103+
temp_headers[name] = value
97104

98-
# Add user ID to headers if available
99105
if hasattr(request.state, "user_id"):
100-
headers["X-User-ID"] = request.state.user_id
106+
temp_headers["X-User-ID"] = str(request.state.user_id)
107+
108+
# Prepare arguments for circuit_breaker.call_service
109+
request_body = await request.body()
110+
111+
if len(request_body) > MAX_REQUEST_BODY_SIZE:
112+
return JSONResponse(
113+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
114+
content={"detail": f"Request body exceeds maximum allowed size of {MAX_REQUEST_BODY_SIZE} bytes."}
115+
)
116+
117+
service_kwargs = {
118+
"headers": temp_headers,
119+
"params": dict(request.query_params)
120+
}
121+
122+
if request.method.upper() not in ("GET", "HEAD", "DELETE"):
123+
service_kwargs["content"] = request_body
101124

102125
# Forward request to service using circuit breaker
103126
response = await circuit_breaker.call_service( # type: ignore
104127
service_name=service_name,
105128
url=target_url,
106129
method=request.method,
107-
headers=headers,
108-
content=body,
109-
params=dict(request.query_params),
130+
**service_kwargs
110131
)
111132

112133
# Return response

backend/api/api_gateway/middleware/auth_middleware.py

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import os
22
from typing import Awaitable, Callable, Optional
33

4-
import httpx
54
from dotenv import load_dotenv
65
from fastapi import HTTPException, Request, status
76
from fastapi.responses import JSONResponse
7+
from jose import ExpiredSignatureError, JWTError, jwt
88

99
# Load environment variables
1010
load_dotenv()
1111

12-
# Auth service URL
13-
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://localhost:8001")
12+
SUPABASE_JWT_SECRET = os.getenv("SUPABASE_JWT_SECRET")
13+
SUPABASE_AUDIENCE = os.getenv("SUPABASE_AUDIENCE", "authenticated")
14+
# Optional: Add SUPABASE_ISSUER if you want to validate the 'iss' claim, e.g.:
15+
# SUPABASE_ISSUER = os.getenv("SUPABASE_ISSUER")
1416

1517

1618
async def auth_middleware(
1719
request: Request, call_next: Callable[[Request], Awaitable[JSONResponse]]
1820
) -> JSONResponse:
21+
if request.method == "OPTIONS":
22+
return await call_next(request)
1923
"""
2024
Middleware for authentication.
2125
@@ -102,56 +106,44 @@ def _get_token_from_request(request: Request) -> Optional[str]:
102106

103107

104108
async def _validate_token(token: str) -> str:
105-
"""
106-
Validate token with auth service.
107-
108-
Args:
109-
token (str): JWT token
110-
111-
Returns:
112-
str: User ID
109+
if not SUPABASE_JWT_SECRET:
110+
print('ERROR: SUPABASE_JWT_SECRET is not configured in the environment.')
111+
raise HTTPException(
112+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
113+
detail='Authentication system configuration error.',
114+
)
113115

114-
Raises:
115-
HTTPException: If token is invalid
116-
"""
117116
try:
118-
# Make request to auth service
119-
async with httpx.AsyncClient() as client:
120-
response = await client.get(
121-
f"{AUTH_SERVICE_URL}/auth/validate",
122-
headers={"Authorization": f"Bearer {token}"},
117+
payload = jwt.decode(
118+
token,
119+
SUPABASE_JWT_SECRET,
120+
algorithms=['HS256'],
121+
audience=SUPABASE_AUDIENCE
122+
# If validating issuer, add: issuer=SUPABASE_ISSUER
123+
)
124+
125+
user_id = payload.get('sub')
126+
if not user_id:
127+
raise HTTPException(
128+
status_code=status.HTTP_401_UNAUTHORIZED,
129+
detail='Invalid token: User ID (sub) not found in token.',
123130
)
131+
132+
return user_id
124133

125-
# Check response
126-
if response.status_code != 200:
127-
raise HTTPException(
128-
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
129-
)
130-
131-
# Parse response
132-
data = response.json()
133-
134-
# Extract user ID from token
135-
# In a real application, you would decode the token and extract the user ID
136-
# For simplicity, we'll assume the auth service returns the user ID
137-
user_id = data.get("user_id")
138-
139-
if not user_id:
140-
raise HTTPException(
141-
status_code=status.HTTP_401_UNAUTHORIZED,
142-
detail="Invalid token, user_id not in response",
143-
)
144-
145-
return user_id
146-
except httpx.RequestError as e:
134+
except ExpiredSignatureError:
135+
raise HTTPException(
136+
status_code=status.HTTP_401_UNAUTHORIZED, detail='Token has expired.'
137+
)
138+
except JWTError as e:
139+
print(f'JWTError during token validation: {str(e)}') # Server log
147140
raise HTTPException(
148-
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
149-
detail=f"Auth service unavailable: {str(e)}",
141+
status_code=status.HTTP_401_UNAUTHORIZED,
142+
detail='Invalid token.',
150143
)
151144
except Exception as e:
152-
# It's good practice to log the error here
153-
# logger.error(f"Unexpected error during token validation with auth service: {str(e)}")
145+
print(f'Unexpected error during token validation: {str(e)}') # Server log
154146
raise HTTPException(
155147
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
156-
detail="An unexpected error occurred while validating the token.",
148+
detail='An unexpected error occurred during token validation.',
157149
)

backend/api/api_gateway/middleware/circuit_breaker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ def __init__(
2323
self,
2424
failure_threshold: int = 5,
2525
recovery_timeout: int = 30,
26-
timeout: float = 5.0,
26+
timeout: float = 10.0,
2727
):
2828
"""
2929
Initialize CircuitBreaker.
3030
3131
Args:
3232
failure_threshold (int, optional): Number of failures before opening circuit. Defaults to 5.
3333
recovery_timeout (int, optional): Seconds to wait before trying again. Defaults to 30.
34-
timeout (float, optional): Request timeout in seconds. Defaults to 5.0.
34+
timeout (float, optional): Request timeout in seconds. Defaults to 10.0.
3535
"""
3636
self.failure_threshold = failure_threshold
3737
self.recovery_timeout = recovery_timeout

backend/api/api_gateway/utils/service_registry.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ def __init__(self):
151151
"methods": ["POST"],
152152
},
153153
{"path": "/health", "methods": ["GET"]},
154+
{"path": "/analytics/card/{card_id}", "methods": ["GET"]},
155+
{"path": "/calendar/events", "methods": ["GET", "POST"]},
156+
{"path": "/ai/inference/{model}", "methods": ["POST"]},
154157
],
155158
},
156159
}

backend/api/auth_service/app/main.py

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from api.auth_service.app.schemas.user import (
99
TokenDTO,
10-
TokenValidationResponseDTO,
10+
# TokenValidationResponseDTO, # No longer needed
1111
UserProfileDTO,
1212
UserRegisterDTO,
1313
)
@@ -67,33 +67,6 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
6767
return auth_service.login(form_data.username, form_data.password)
6868

6969

70-
@app.get(
71-
"/auth/validate", response_model=TokenValidationResponseDTO, tags=["Authentication"]
72-
)
73-
async def validate(token: str = Security(oauth2_scheme)):
74-
"""
75-
Validate a token. Also returns user_id along with new tokens.
76-
77-
Args:
78-
token (str): JWT token
79-
"""
80-
return auth_service.validate_token(token)
81-
82-
83-
@app.post("/auth/refresh", response_model=TokenDTO, tags=["Authentication"])
84-
async def refresh(refresh_token: str) -> Any:
85-
"""
86-
Refresh a token.
87-
88-
Args:
89-
refresh_token (str): Refresh token
90-
91-
Returns:
92-
TokenDTO: Authentication tokens
93-
"""
94-
return auth_service.refresh_token(refresh_token)
95-
96-
9770
@app.post("/auth/logout", tags=["Authentication"])
9871
async def logout(token: str = Security(oauth2_scheme)):
9972
"""
@@ -131,3 +104,4 @@ async def health_check() -> Any:
131104
Dict[str, str]: Health status
132105
"""
133106
return {"status": "healthy"}
107+

0 commit comments

Comments
 (0)