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.
187 changes: 176 additions & 11 deletions backend/api_gateway/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,36 @@
This Flask application aggregates endpoints from various microservices.
"""

from flask import Flask, jsonify, request
from flask import Flask, jsonify, request, make_response
from flask_cors import CORS
from flask_restx import Api, Resource, fields
import sys
import os
import jwt
import json
import uuid
import datetime
from functools import wraps
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))

# load env
from dotenv import load_dotenv
load_dotenv()



from backend.microservices.summarization_service import run_summarization, process_articles
from backend.microservices.news_fetcher import fetch_news
from backend.core.config import Config
from backend.core.utils import setup_logger, log_exception

from backend.microservices.auth_service import load_users
from backend.microservices.news_storage import store_article_in_supabase, log_user_search
# Initialize logger
logger = setup_logger(__name__)

# Initialize Flask app with CORS support
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'your-secret-key') # Change this in production
CORS(app, origins=Config.CORS_ORIGINS, supports_credentials=True, allow_headers=['Content-Type', 'Authorization'])

# Initialize Flask-RestX
Expand All @@ -30,12 +43,46 @@
news_ns = api.namespace('api/news', description='News operations')
health_ns = api.namespace('health', description='Health check operations')
summarize_ns = api.namespace('summarize', description='Text summarization operations')
user_ns = api.namespace('api/user', description='User operations')
auth_ns = api.namespace('api/auth', description='Authentication operations')

def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header:
return {'error': 'Authorization header missing'}, 401
try:
token = auth_header.split()[1]
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
return f(*args, **kwargs)
except Exception as e:
return {'error': 'Invalid token', 'message': str(e)}, 401
return decorated

# Define models for documentation
article_model = api.model('Article', {
'article_text': fields.String(required=True, description='The text to summarize')
})

user_profile_model = api.model('UserProfile', {
'id': fields.String(description='User ID'),
'username': fields.String(description='Username'),
'email': fields.String(description='Email address'),
'firstName': fields.String(description='First name'),
'lastName': fields.String(description='Last name'),
'avatarUrl': fields.String(description='URL to user avatar')
})

# Model for user signup
signup_model = api.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')
})

# Health check endpoint
@health_ns.route('/')
class HealthCheck(Resource):
Expand All @@ -54,27 +101,46 @@ def post(self):
summary = run_summarization(article_text)
return {"summary": summary}, 200

# News fetch endpoint
@news_ns.route('/fetch')
class NewsFetch(Resource):
@news_ns.param('keyword', 'Search keyword for news')
@news_ns.param('user_id', 'User ID for logging search history')
@news_ns.param('session_id', 'Session ID for tracking requests')
def get(self):
"""Fetch news articles based on keyword"""
"""
Fetch news articles, store them in Supabase, and log user search history if a user ID is provided.
"""
try:
keyword = request.args.get('keyword', '')
user_id = request.args.get('user_id') # optional
session_id = request.args.get('session_id')
articles = fetch_news(keyword, session_id)
return {

# Fetch articles from your existing news_fetcher module.
articles = fetch_news(keyword) # This returns a list of articles.
stored_article_ids = []

for article in articles:
# Store each article in the database; get its unique id.
article_id = store_article_in_supabase(article)
stored_article_ids.append(article_id)

# If the request included a user_id, log the search for this article.
if user_id:
log_user_search(user_id, article_id, session_id)

return make_response(jsonify({
'status': 'success',
'data': articles,
'session_id': session_id
}, 200
'data': stored_article_ids
}), 200)

except Exception as e:
return {
return make_response(jsonify({
'status': 'error',
'message': str(e)
}, 500
}), 500)




# News processing endpoint
@news_ns.route('/process')
Expand All @@ -98,6 +164,105 @@ def post(self):
'message': str(e)
}, 500

# User authentication endpoints
@auth_ns.route('/signup')
class Signup(Resource):
@auth_ns.expect(signup_model)
def post(self):
print('signup')
"""Register a new user"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
email = data.get('email')
firstName = data.get('firstName', '')
lastName = data.get('lastName', '')

if not username or not password or not email:
return {'error': 'Username, password, and email are required'}, 400

users = load_users()

# Check if username already exists
if any(u.get('username') == username for u in users):
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
}

print(new_user)

users.append(new_user)

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

# Generate JWT token
token = jwt.encode({
'id': new_user['id'],
'username': new_user['username'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, app.config['SECRET_KEY'], algorithm='HS256')

# Exclude password from response
user_data = {k: new_user[k] for k in new_user if k != 'password'}
return {'message': 'User registered successfully', 'user': user_data, 'token': token}, 201

@auth_ns.route('/login')
class Login(Resource):
def post(self):
"""Login and get authentication token"""
print('login in')
data = request.get_json()
username = data.get('username')
password = data.get('password')

if not username or not password:
return {'error': 'Username and password are required'}, 400

users = load_users()
user = next((u for u in users if u.get('username') == username and u.get('password') == password), None)

if not user:
return {'error': 'Invalid credentials'}, 401

token = jwt.encode({
'id': user['id'],
'username': user['username'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, app.config['SECRET_KEY'], algorithm='HS256')

user_data = {k: user[k] for k in user if k != 'password'}
return {'token': token, 'user': user_data}

@user_ns.route('/profile')
class UserProfile(Resource):
@token_required
@user_ns.marshal_with(user_profile_model)
def get(self):
"""Get user profile information"""
auth_header = request.headers.get('Authorization')
token = auth_header.split()[1]
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])

users = load_users()
user = next((u for u in users if u.get('id') == payload.get('id')), None)
if not user:
return {'error': 'User not found'}, 404

return {k: user[k] for k in user if k != 'password'}, 200

if __name__ == '__main__':
port = int(sys.argv[1]) if len(sys.argv) > 1 else Config.API_PORT
app.run(host=Config.API_HOST, port=port, debug=True)
3 changes: 2 additions & 1 deletion backend/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ class Config:
NEWS_API_KEY = os.getenv('NEWS_API_KEY')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')


# CORS Configuration
CORS_ORIGINS = os.getenv('CORS_ORIGINS', '*').split(',')

print("CORS_ORIGINS", CORS_ORIGINS)
# Logging Configuration
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
Expand Down
26 changes: 26 additions & 0 deletions backend/data/users.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"id": 1,
"username": "testuser",
"password": "password123",
"email": "test@example.com",
"firstName": "Test",
"lastName": "User"
},
{
"id": "92e4c2fe-890d-49df-b9e7-9ce84498ca71",
"username": "akal",
"password": "akal",
"email": "akal@akal.com",
"firstName": "",
"lastName": ""
},
{
"id": "68e583ee-e9e8-44bc-9642-0344429b0c0c",
"username": "user",
"password": "user",
"email": "user@user.com",
"firstName": "",
"lastName": ""
}
]
Binary file modified backend/microservices/.DS_Store
Binary file not shown.
Binary file not shown.
95 changes: 95 additions & 0 deletions backend/microservices/auth_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
auth_service.py - Microservice for Authentication
Handles user authentication, JWT token generation, and user profile management.
"""

from flask import Flask, request, jsonify
import json
import datetime
import jwt
import os
from pathlib import Path

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'your-secret-key') # Change this in production

# Get the path to the users file
USERS_FILE = Path(__file__).parent.parent / 'data' / 'users.txt'

# Ensure the data directory exists
USERS_FILE.parent.mkdir(parents=True, exist_ok=True)

# Create users.txt if it doesn't exist
if not USERS_FILE.exists():
with open(USERS_FILE, 'w') as f:
json.dump([
{
"id": 1,
"username": "testuser",
"password": "password123",
"email": "test@example.com",
"firstName": "Test",
"lastName": "User"
}
], f)

def load_users():
"""Load users from the users.txt file"""
try:
with open(USERS_FILE, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Error loading users: {e}")
return []

# @app.route('/api/auth/login', methods=['POST'])
# def login():
# data = request.get_json()
# username = data.get('username')
# password = data.get('password')

# if not username or not password:
# return jsonify({'error': 'Username and password are required'}), 400

# users = load_users()
# user = next((u for u in users if u.get('username') == username and u.get('password') == password), None)

# if not user:
# return jsonify({'error': 'Invalid credentials'}), 401

# token = jwt.encode({
# 'id': user['id'],
# 'username': user['username'],
# 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
# }, app.config['SECRET_KEY'], algorithm='HS256')

# user_data = {k: user[k] for k in user if k != 'password'}
# return jsonify({'token': token, 'user': user_data})

# @app.route('/api/user/profile', methods=['GET'])
# def profile():
# auth_header = request.headers.get('Authorization')
# if not auth_header:
# return jsonify({'error': 'Authorization header missing'}), 401

# try:
# token = auth_header.split()[1]
# payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
# except Exception as e:
# return jsonify({'error': 'Invalid token', 'message': str(e)}), 401

# users = load_users()
# user = next((u for u in users if u.get('id') == payload.get('id')), None)
# if not user:
# return jsonify({'error': 'User not found'}), 404

# user_data = {k: user[k] for k in user if k != 'password'}
# return jsonify(user_data)

# @app.route('/health', methods=['GET'])
# def health():
# return jsonify({'status': 'Authentication service is healthy'}), 200

# if __name__ == '__main__':
# app.run(host='0.0.0.0', port=5003, debug=True)
Loading
Loading