diff --git a/.DS_Store b/.DS_Store index d5a9a66..04725a5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/api_gateway/api_gateway.py b/backend/api_gateway/api_gateway.py index e05b3fe..4e5c85d 100644 --- a/backend/api_gateway/api_gateway.py +++ b/backend/api_gateway/api_gateway.py @@ -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 @@ -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): @@ -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') @@ -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) diff --git a/backend/core/config.py b/backend/core/config.py index b35a252..3116038 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -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' diff --git a/backend/data/users.txt b/backend/data/users.txt new file mode 100644 index 0000000..939f45c --- /dev/null +++ b/backend/data/users.txt @@ -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": "" + } +] \ No newline at end of file diff --git a/backend/microservices/.DS_Store b/backend/microservices/.DS_Store index d5dffc4..677c187 100644 Binary files a/backend/microservices/.DS_Store and b/backend/microservices/.DS_Store differ diff --git a/backend/microservices/__pycache__/summarization_service.cpython-312.pyc b/backend/microservices/__pycache__/summarization_service.cpython-312.pyc index daa2d7f..8a0c2c1 100644 Binary files a/backend/microservices/__pycache__/summarization_service.cpython-312.pyc and b/backend/microservices/__pycache__/summarization_service.cpython-312.pyc differ diff --git a/backend/microservices/auth_service.py b/backend/microservices/auth_service.py new file mode 100644 index 0000000..bf233e6 --- /dev/null +++ b/backend/microservices/auth_service.py @@ -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) \ No newline at end of file diff --git a/backend/microservices/news_fetcher.py b/backend/microservices/news_fetcher.py index 60e20c3..6fa336c 100644 --- a/backend/microservices/news_fetcher.py +++ b/backend/microservices/news_fetcher.py @@ -33,11 +33,12 @@ def fetch_news(keyword='', session_id=None): if not articles: print("No articles found for the given keyword.") else: + pass # Use session_id in the filename if provided - if session_id: - write_to_file(articles, session_id) - else: - write_to_file(articles) + # if session_id: + # write_to_file(articles, session_id) + # else: + # write_to_file(articles) # for article in articles: # print(f"Title: {article['title']}") # print(f"Description: {article['description']}") diff --git a/backend/microservices/news_storage.py b/backend/microservices/news_storage.py new file mode 100644 index 0000000..35861a5 --- /dev/null +++ b/backend/microservices/news_storage.py @@ -0,0 +1,49 @@ +# backend/microservices/news_storage.py + +import os +import datetime +from supabase import create_client, Client # Make sure you're using supabase-py or your preferred client +from dotenv import load_dotenv + +load_dotenv('../../.env') + +# Use your service key here for secure server-side operations. +SUPABASE_URL = os.getenv("VITE_SUPABASE_URL") +SUPABASE_SERVICE_KEY = os.getenv("VITE_SUPABASE_ANON_KEY") +supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_KEY) + +def store_article_in_supabase(article): + """ + Inserts a news article into the Supabase news_articles table if it doesn't already exist. + Uniqueness is enforced by the URL field (which is UNIQUE in the table). + """ + # Check if the article already exists using the URL as unique identifier. + existing = supabase.table("news_articles").select("*").eq("url", article["url"]).execute() + if existing.data and len(existing.data) > 0: + # Article already exists; return its id. + return existing.data[0]["id"] + else: + # Insert a new article. + result = supabase.table("news_articles").insert({ + "title": article["title"], + "summary": article.get("summary", ""), + "content": article.get("content", ""), + # The source can be a dict (from API) or a plain string. + "source": article["source"]["name"] if isinstance(article.get("source"), dict) else article["source"], + "published_at": article["publishedAt"], + "url": article["url"], + "image": article.get("urlToImage", "") + }).execute() + return result.data[0]["id"] + +def log_user_search(user_id, news_id, session_id): + """ + Logs a search event by inserting a record into the user_search_history join table. + """ + result = supabase.table("user_search_history").insert({ + "user_id": user_id, + "news_id": news_id, + "searched_at": datetime.datetime.utcnow().isoformat(), + "session_id": session_id, + }).execute() + return result \ No newline at end of file diff --git a/backend/microservices/.env b/backend/microservices/nope.env similarity index 100% rename from backend/microservices/.env rename to backend/microservices/nope.env diff --git a/backend/microservices/summarization_service.py b/backend/microservices/summarization_service.py index a6f9e3a..f2a5547 100755 --- a/backend/microservices/summarization_service.py +++ b/backend/microservices/summarization_service.py @@ -10,6 +10,7 @@ from backend.core.config import Config from backend.core.utils import setup_logger, log_exception import yake +import os # Initialize logger logger = setup_logger(__name__) @@ -17,6 +18,16 @@ # Configure OpenAI openai.api_key = Config.OPENAI_API_KEY client = openai.OpenAI() +from supabase import create_client, Client # Make sure you're using supabase-py or your preferred client + +from dotenv import load_dotenv +load_dotenv('../../.env') + +# Use your service key here for secure server-side operations. +SUPABASE_URL = os.getenv("VITE_SUPABASE_URL") +SUPABASE_SERVICE_KEY = os.getenv("VITE_SUPABASE_ANON_KEY") +supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_KEY) + @log_exception(logger) def fetch_article_content(url): @@ -87,52 +98,60 @@ def get_keywords(text,num_keywords=1): @log_exception(logger) -def process_articles(session_id=None): +def process_articles(session_id): try: - # Use session_id for file naming, default to 'default' if not provided - if not session_id: - session_id = 'default' - file_name = f'{session_id}_news_data.json' - news_data_path = Config.NEWS_DATA_DIR / file_name + # Query only articles that belong to the current session. + # result = supabase.table("news_articles").select("*").eq("session_id", session_id).execute() + # articles = result.data + + # First, query the user_search_history table for records with this session_id. + history_result = supabase.table("user_search_history").select("news_id").eq("session_id", session_id).execute() + article_ids = [record["news_id"] for record in history_result.data] - with open(news_data_path, 'r') as file: - articles = json.load(file) + # Now, query the news_articles table for those article IDs. + articles = [] + if article_ids: + result = supabase.table("news_articles").select("*").in_("id", article_ids).execute() + articles = result.data summarized_articles = [] for article in articles: logger.info(f"Processing article: {article['title']}") - # Fetch full article content from URL - content = fetch_article_content(article['url']) + content = article.get('content') + if not content: + content = fetch_article_content(article['url']) + if content: summary = run_summarization(content) else: summary = run_summarization(article.get('content', '')) - + summarized_articles.append({ 'title': article['title'], 'author': article.get('author', 'Unknown Author'), - 'source': article['source']['name'], - 'publishedAt': article['publishedAt'], + 'source': article.get('source'), + 'publishedAt': article.get('published_at'), 'url': article['url'], - 'urlToImage': article.get('urlToImage'), + 'urlToImage': article.get('image'), 'content': article.get('content', ''), 'summary': summary, 'filter_keywords': get_keywords(article.get('content', '')) }) - # Save summarized articles to configured path with session_id - output_file = f'{session_id}_summarized_news.json' - output_path = Config.SUMMARIZED_NEWS_DIR / output_file - output_path.parent.mkdir(parents=True, exist_ok=True) - with open(output_path, 'w') as file: - json.dump(summarized_articles, file, indent=4) - logger.info(f"Summarized articles saved to {output_path}") + # Optionally, save or return the summarized articles. + # output_file = f'{session_id}_summarized_news.json' + # output_path = Config.get_summarized_news_path() / output_file + # with open(output_path, 'w') as file: + # json.dump(summarized_articles, file, indent=4) + # logger.info(f"Summarized articles saved to {output_path}") return summarized_articles except Exception as e: logger.error(f"Error processing articles: {str(e)}") + raise e + if __name__ == '__main__': process_articles() diff --git a/requirements.txt b/requirements.txt index 5a55291..6287bf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ requests beautifulsoup4 feedparser -transformer +# transformer torch # Summarization & Clustering diff --git a/supabase sql policy b/supabase sql policy new file mode 100644 index 0000000..17ef574 --- /dev/null +++ b/supabase sql policy @@ -0,0 +1,44 @@ +-- Drop the table if it already exists +DROP TABLE IF EXISTS public.profiles CASCADE; + +-- Create the profiles table +CREATE TABLE public.profiles ( + id uuid PRIMARY KEY, -- Should match the authenticated user's id (auth.uid()) + email text, + display_name text, + avatar_url text, + bio text, + website text, + location text, + preference jsonb, -- Using jsonb for flexibility + created_at timestamptz DEFAULT now(), + updated_at timestamptz +); + +-- Enable Row-Level Security on the table +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; + +-- Drop any existing policy named "Users can manage their own profile" +DROP POLICY IF EXISTS "Users can manage their own profile" ON public.profiles; + +-- Create a unified policy that applies to all operations +CREATE POLICY "Users can manage their own profile" + ON public.profiles + FOR ALL + USING ( auth.uid() IS NOT NULL AND auth.uid() = id ) + WITH CHECK ( auth.uid() IS NOT NULL AND auth.uid() = id ); + + + + + +1. delete the user from supabase and then try to run the query, it'll run successfully which means the app is not checking if the user is there in the supabase, +it justs checks for the cookie, or JWT or something. basically copying local storage info in network tab will log the user in? +2. Add header in all the pages which has sign in and sign out functionality +3. Make profile page better + 3.1 Make the profile picture and image + 3.2 Remove Avatar URL field + + + +combine Header.tsx and LandingHeader.tsx such that it has different funcionatlity if rendered in LandingPage.tsx and in NewsApp.tsx \ No newline at end of file