Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ EXPOSE 8080

# Run the API Gateway.
# Cloud Run will set PORT, so we use that environment variable.
CMD ["python", "backend/api_gateway/api_gateway.py"]
# CMD ["python", "backend/api_gateway/api_gateway.py"]
CMD ["./start-services.sh"]
822 changes: 42 additions & 780 deletions backend/api_gateway/api_gateway.py

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions backend/api_gateway/routes/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Authentication API Routes

This module contains the API routes for authentication operations including signup, login, and token management.
"""

# Standard library imports
from flask import jsonify, request, make_response
from flask_restx import Resource, Namespace, fields
import jwt
import uuid
import datetime
import os
import json
from functools import wraps

# Import microservices and utilities
from backend.microservices.auth_service import load_users
from backend.core.utils import setup_logger

# Initialize logger
logger = setup_logger(__name__)

# Create auth namespace
auth_ns = Namespace('api/auth', description='Authentication operations')

# Define API models for request/response documentation
signup_model = auth_ns.model('Signup', {
'username': fields.String(required=True, description='Username'),
'password': fields.String(required=True, description='Password'),
'email': fields.String(required=True, description='Email address'),
'firstName': fields.String(required=False, description='First name'),
'lastName': fields.String(required=False, description='Last name')
})

@auth_ns.route('/signup')
class Signup(Resource):
@auth_ns.expect(signup_model)
def post(self):
"""Register a new user in the system.

Creates a new user account with the provided information and generates
a JWT token for immediate authentication.

Expected JSON payload:
{
'username': str (required),
'password': str (required),
'email': str (required),
'firstName': str (optional),
'lastName': str (optional)
}

Returns:
dict: Contains user data (excluding password) and JWT token.
int: HTTP 201 on success, 400 on validation error, 500 on server error.
"""
logger.info("User signup endpoint called")
data = request.get_json()
username = data.get('username')
password = data.get('password')
email = data.get('email')
firstName = data.get('firstName', '')
lastName = data.get('lastName', '')
logger.info(f"Signup request for username: {username}, email: {email}")

if not username or not password or not email:
logger.warning("Signup validation failed: missing required fields")
return {'error': 'Username, password, and email are required'}, 400

users = load_users()
logger.debug(f"Loaded {len(users)} existing users")

# Check if username already exists
if any(u.get('username') == username for u in users):
logger.warning(f"Signup failed: Username {username} already exists")
return {'error': 'Username already exists'}, 400

# Create new user with unique ID
new_user = {
'id': str(uuid.uuid4()),
'username': username,
'password': password,
'email': email,
'firstName': firstName,
'lastName': lastName
}
logger.debug(f"Created new user with ID: {new_user['id']}")

users.append(new_user)

try:
# Save updated users list
logger.debug("Saving updated users list")
with open(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data', 'users.txt'), 'w') as f:
json.dump(users, f, indent=4)
logger.debug("Users list saved successfully")
except Exception as e:
logger.error(f"Error saving user data: {str(e)}")
return {'error': 'Failed to save user data', 'message': str(e)}, 500

# Generate JWT token
logger.debug("Generating JWT token")
from flask import current_app
token = jwt.encode({
'sub': new_user['id'],
'username': new_user['username'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'aud': 'authenticated'
}, current_app.config['SECRET_KEY'], algorithm='HS256')
logger.debug(f"Token generated: {token[:10]}...")

# Exclude password from response
user_data = {k: new_user[k] for k in new_user if k != 'password'}
logger.info("Signup successful")
return {'message': 'User registered successfully', 'user': user_data, 'token': token}, 201

@auth_ns.route('/login')
class Login(Resource):
def post(self):
"""Authenticate user and generate JWT token.

Validates user credentials and generates a JWT token for authenticated access.

Expected JSON payload:
{
'username': str (required),
'password': str (required)
}

Returns:
dict: Contains user data (excluding password) and JWT token.
int: HTTP 200 on success, 400 on validation error, 401 on invalid credentials.
"""
logger.info("Login endpoint called")
data = request.get_json()
username = data.get('username')
password = data.get('password')
logger.info(f"Login attempt for username: {username}")

if not username or not password:
logger.warning("Login validation failed: missing username or password")
return {'error': 'Username and password are required'}, 400

users = load_users()
logger.debug(f"Loaded {len(users)} users")
user = next((u for u in users if u.get('username') == username and u.get('password') == password), None)

if not user:
logger.warning(f"Invalid credentials for username: {username}")
return {'error': 'Invalid credentials'}, 401

logger.debug(f"Valid credentials for user: {user.get('id')}")
logger.debug("Generating JWT token")
from flask import current_app
token = jwt.encode({
'sub': user['id'],
'username': user['username'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'aud': 'authenticated'
}, current_app.config['SECRET_KEY'], algorithm='HS256')
logger.debug(f"Token generated: {token[:10]}...")

user_data = {k: user[k] for k in user if k != 'password'}
logger.info("Login successful")
return {'token': token, 'user': user_data}
155 changes: 155 additions & 0 deletions backend/api_gateway/routes/bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
Bookmark API Routes

This module contains the API routes for bookmark operations including adding, listing, and deleting bookmarks.
"""

# Standard library imports
from flask import jsonify, request, make_response
from flask_restx import Resource, Namespace
import jwt
from functools import wraps
from flask import current_app

# Import microservices and utilities
from backend.microservices.news_storage import add_bookmark, get_user_bookmarks, delete_bookmark
from backend.core.utils import setup_logger

# Initialize logger
logger = setup_logger(__name__)

# Create bookmark namespace
bookmark_ns = Namespace('api/bookmarks', description='Bookmark operations')

# Import token_required decorator from utils
from backend.api_gateway.utils.auth import token_required

@bookmark_ns.route('/')
class Bookmark(Resource):
@token_required
def get(self):
"""Retrieve all bookmarks for the authenticated user.

Requires a valid JWT token in the Authorization header.
Returns a list of bookmarked articles for the current user.

Returns:
dict: Contains list of bookmarked articles and success status.
int: HTTP 200 on success, 500 on error.
"""
try:
logger.info("Get bookmarks endpoint called")
auth_header = request.headers.get('Authorization')
token = auth_header.split()[1]
logger.debug(f"Decoding token: {token[:10]}...")
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'], audience='authenticated')
user_id = payload.get('sub')
logger.info(f"Getting bookmarks for user: {user_id}")

bookmarks = get_user_bookmarks(user_id)
logger.debug(f"Found {len(bookmarks)} bookmarks")

return {
'status': 'success',
'data': bookmarks
}, 200

except Exception as e:
logger.error(f"Error fetching bookmarks: {str(e)}")
return {
'status': 'error',
'message': str(e)
}, 500

@token_required
def post(self):
"""Add a new bookmark for the authenticated user.

Requires a valid JWT token in the Authorization header.
Creates a bookmark linking the user to a specific news article.

Expected JSON payload:
{
'news_id': str (required)
}

Returns:
dict: Contains bookmark ID and success status.
int: HTTP 201 on success, 400 on validation error, 500 on server error.
"""
try:
logger.info("Add bookmark endpoint called")
auth_header = request.headers.get('Authorization')
token = auth_header.split()[1]
logger.debug(f"Decoding token: {token[:10]}...")
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'], audience='authenticated')
user_id = payload.get('sub')
logger.info(f"Adding bookmark for user: {user_id}")

data = request.get_json()
news_id = data.get('news_id')
logger.debug(f"News article ID: {news_id}")

if not news_id:
logger.warning("News article ID missing in request")
return {'error': 'News article ID is required'}, 400

logger.info(f"Adding bookmark for user {user_id}, article {news_id}")
bookmark = add_bookmark(user_id, news_id)
logger.debug(f"Bookmark added with ID: {bookmark['id'] if isinstance(bookmark, dict) else bookmark}")

return {
'status': 'success',
'message': 'Bookmark added successfully',
'data': {
'bookmark_id': bookmark['id'] if isinstance(bookmark, dict) else bookmark
}
}, 201

except Exception as e:
logger.error(f"Error adding bookmark: {str(e)}")
return {
'status': 'error',
'message': str(e)
}, 500

@bookmark_ns.route('/<string:bookmark_id>')
class BookmarkDelete(Resource):
@token_required
def delete(self, bookmark_id):
"""Remove a bookmark for a news article.

Requires a valid JWT token in the Authorization header.
Deletes the specified bookmark for the authenticated user.

Args:
bookmark_id (str): The ID of the bookmark to be deleted.

Returns:
dict: Contains success message.
int: HTTP 200 on success, 500 on error.
"""
try:
logger.info(f"Delete bookmark endpoint called for bookmark: {bookmark_id}")
auth_header = request.headers.get('Authorization')
token = auth_header.split()[1]
logger.debug(f"Decoding token: {token[:10]}...")
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'], audience='authenticated')
user_id = payload.get('sub')
logger.info(f"Deleting bookmark {bookmark_id} for user {user_id}")

result = delete_bookmark(user_id, bookmark_id)
logger.debug(f"Deletion result: {result}")

return {
'status': 'success',
'message': 'Bookmark removed successfully'
}, 200

except Exception as e:
logger.error(f"Error removing bookmark: {str(e)}")
return {
'status': 'error',
'message': str(e)
}, 500
29 changes: 29 additions & 0 deletions backend/api_gateway/routes/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
Health API Routes

This module contains the API routes for health check operations.
"""

# Standard library imports
from flask import jsonify, request
from flask_restx import Resource, Namespace
from backend.core.utils import setup_logger

# Initialize logger
logger = setup_logger(__name__)

# Create health namespace
health_ns = Namespace('health', description='Health check operations')

@health_ns.route('/')
class HealthCheck(Resource):
def get(self):
"""Check the health status of the API Gateway.

Returns:
dict: A dictionary containing the health status.
int: HTTP 200 status code indicating success.
"""
logger.info("Health check endpoint called")
return {"status": "API Gateway is healthy"}, 200
Loading
Loading