Skip to content

Commit bad7103

Browse files
author
Aarushi Keshari
committed
fix: resolve merge conflicts with upstream/main
2 parents cefd0e6 + 058fe86 commit bad7103

25 files changed

Lines changed: 1408 additions & 445 deletions

‎Scraper/README.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Book Metadata Scraper
2+
3+
A Python-based scraper/API integration project that fetches metadata for books using the Google Books API.
4+
5+
## Features
6+
7+
- Fetch book title
8+
- Author name
9+
- Genre
10+
- Summary/description
11+
- Cover image URL
12+
- Ratings
13+
- Save data into JSON and CSV
14+
- Duplicate detection
15+
16+
## Tech Stack
17+
18+
- Python
19+
- Requests
20+
- JSON
21+
- CSV
22+
23+
## Installation
24+
25+
```bash
26+
pip install -r requirements.txt

‎Scraper/books.csv‎

Whitespace-only changes.

‎Scraper/books.json‎

Whitespace-only changes.

‎Scraper/main.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from scraper import fetch_book_data
2+
from utils import save_to_json, save_to_csv
3+
4+
5+
def main():
6+
7+
print("===== Book Metadata Scraper =====")
8+
9+
while True:
10+
11+
book_name = input("\nEnter book name (or type 'exit'): ")
12+
13+
if book_name.lower() == "exit":
14+
print("Exiting program...")
15+
break
16+
17+
book_data = fetch_book_data(book_name)
18+
19+
if book_data:
20+
21+
print("\nBook Data Fetched Successfully:\n")
22+
23+
for key, value in book_data.items():
24+
print(f"{key}: {value}")
25+
26+
#Save the Date
27+
save_to_json(book_data)
28+
save_to_csv(book_data)
29+
30+
else:
31+
print("Could not fetch book data.")
32+
33+
34+
if __name__ == "__main__":
35+
main()

‎Scraper/requirements.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
requests
2+
pandas

‎Scraper/scraper.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import requests
2+
3+
def fetch_book_data(book_name):
4+
"""
5+
Fetch metadata for a book using Google Books API
6+
"""
7+
8+
url = f"https://www.googleapis.com/books/v1/volumes?q={book_name}"
9+
10+
try:
11+
response = requests.get(url)
12+
13+
# Check if request successful
14+
if response.status_code != 200:
15+
print("Error fetching data")
16+
return None
17+
18+
data = response.json()
19+
20+
# Check if books found
21+
if "items" not in data:
22+
print(f"No results found for '{book_name}'")
23+
return None
24+
25+
book = data["items"][0]["volumeInfo"]
26+
27+
# Extract required fields safely
28+
book_data = {
29+
"title": book.get("title", "N/A"),
30+
"authors": ", ".join(book.get("authors", ["Unknown"])),
31+
"genre": ", ".join(book.get("categories", ["Unknown"])),
32+
"description": book.get("description", "No description available"),
33+
"thumbnail": book.get("imageLinks", {}).get("thumbnail", "No image"),
34+
"rating": book.get("averageRating", "Not Rated")
35+
}
36+
37+
return book_data
38+
39+
except Exception as e:
40+
print("Error:", e)
41+
return None

‎Scraper/utils.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import json
2+
import csv
3+
import os
4+
5+
6+
JSON_FILE = "books.json"
7+
CSV_FILE = "books.csv"
8+
9+
10+
def load_existing_books():
11+
"""
12+
Load existing books from JSON file
13+
"""
14+
15+
if not os.path.exists(JSON_FILE):
16+
return []
17+
18+
with open(JSON_FILE, "r") as file:
19+
try:
20+
return json.load(file)
21+
except:
22+
return []
23+
24+
25+
def is_duplicate(title, existing_books):
26+
"""
27+
Check duplicate book titles
28+
"""
29+
30+
for book in existing_books:
31+
if book["title"].lower() == title.lower():
32+
return True
33+
34+
return False
35+
36+
37+
def save_to_json(book_data):
38+
"""
39+
Save book data into JSON
40+
"""
41+
42+
existing_books = load_existing_books()
43+
44+
if is_duplicate(book_data["title"], existing_books):
45+
print("Duplicate book found. Skipping...")
46+
return
47+
48+
existing_books.append(book_data)
49+
50+
with open(JSON_FILE, "w") as file:
51+
json.dump(existing_books, file, indent=4)
52+
53+
print("Book saved to JSON successfully.")
54+
55+
56+
def save_to_csv(book_data):
57+
"""
58+
Save book data into CSV
59+
"""
60+
61+
file_exists = os.path.exists(CSV_FILE)
62+
63+
with open(CSV_FILE, "a", newline='', encoding='utf-8') as file:
64+
writer = csv.DictWriter(file, fieldnames=book_data.keys())
65+
66+
# Write header only once
67+
if not file_exists:
68+
writer.writeheader()
69+
70+
writer.writerow(book_data)
71+
72+
print("Book saved to CSV successfully.")

‎backend/ai_service.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ def process_new_book(book_data):
4141
cache_recommendations,
4242
cache_mood_tags,
4343
cache_chat_response,
44-
cache_mood_analysis
44+
cache_mood_analysis,
45+
cache_category_books,
4546
)
4647

4748
# Setup logging from environment
@@ -724,7 +725,7 @@ def get_ai_recommendations(query):
724725

725726
return f"Based on your interest in '{query}', I'd recommend exploring books that capture similar themes and emotional resonance."
726727

727-
728+
@cache_category_books
728729
def get_category_books(category: str, vibe_description: str, count: int = 5) -> list:
729730
"""
730731
Generate a list of real, relevant books for a specific shelf category.

‎backend/app.py‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from flask_limiter.util import get_remote_address
1212
from sqlalchemy.orm import joinedload
1313
from flask_migrate import Migrate
14+
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
1415
from sqlalchemy.exc import SQLAlchemyError
1516
from dotenv import load_dotenv
1617
from backend.spine_generator import create_spine
@@ -111,6 +112,28 @@
111112
# Apply configuration to Flask app
112113
app.config.update(app_config.flask_config)
113114

115+
# =====================================================================
116+
# SECURITY COMPLIANCE UPDATE: CSRF PROTECTION (FLASK-WTF)
117+
# =====================================================================
118+
# Cross-Site Request Forgery (CSRF) is a serious vulnerability where
119+
# an attacker tricks a user into performing actions they didn't intend
120+
# to do on a different website where they are authenticated.
121+
#
122+
# While JWT-Extended provides CSRF protection for authenticated
123+
# requests via cookies, the initial authentication flow (Login/Register)
124+
# often remains vulnerable if not explicitly protected.
125+
#
126+
# We initialize Flask-WTF's CSRFProtect to provide a secondary layer
127+
# of defense. This will automatically validate CSRF tokens for all
128+
# POST, PUT, PATCH, and DELETE requests.
129+
# =====================================================================
130+
csrf = CSRFProtect(app)
131+
132+
# Exclude certain endpoints from global CSRF if they are handled by JWT CSRF
133+
# or if they are intended to be public-facing without token requirements.
134+
# In this architecture, we prefer explicit protection on all mutation routes.
135+
# csrf.exempt(some_blueprint)
136+
114137
# Initialize JWT Manager
115138
jwt = JWTManager(app)
116139

@@ -152,6 +175,30 @@ def page_not_found(e: Exception):
152175
return app.send_static_file('404.html'), 404
153176

154177

178+
@app.errorhandler(CSRFError)
179+
def handle_csrf_error(e):
180+
"""
181+
=========================================================================
182+
CUSTOM CSRF ERROR HANDLER
183+
=========================================================================
184+
Intercepts CSRF validation failures and returns a standardized JSON
185+
error response instead of the default HTML error page.
186+
187+
This is critical for a RESTful API architecture where the client
188+
expects consistent JSON structures even during security failures.
189+
190+
Status: 400 Bad Request (as per Flask-WTF default for CSRF failures)
191+
=========================================================================
192+
"""
193+
logger.warning(f"CSRF Validation Failed: {e.description} | Remote IP: {request.remote_addr}")
194+
return jsonify({
195+
"success": False,
196+
"error": "CSRF_VALIDATION_FAILED",
197+
"message": f"Security token validation failed: {e.description}. Please refresh the page.",
198+
"code": 400
199+
}), 400
200+
201+
155202
@app.after_request
156203
def add_security_headers(response):
157204
"""
@@ -309,6 +356,25 @@ def get_config():
309356
"google_books_key_secondary": os.getenv('GOOGLE_BOOKS_API_KEY_SECONDARY', '')
310357
})
311358

359+
# =====================================================================
360+
# ENDPOINT: CSRF Token Retrieval
361+
# =====================================================================
362+
# Since this is a decoupled frontend, we need a way for the client-side
363+
# application to "prime" itself with a valid CSRF token before attempting
364+
# a state-mutating request (like Login or Register).
365+
#
366+
# This endpoint generates a new token and sets the associated session
367+
# cookie. The frontend should call this on page load of sensitive forms.
368+
# =====================================================================
369+
@app.route('/api/v1/csrf-token', methods=['GET'])
370+
def get_csrf_token():
371+
"""
372+
Generate and return a fresh CSRF token.
373+
The token is automatically tied to the user's session.
374+
"""
375+
token = generate_csrf()
376+
return success_response(data={"csrf_token": token})
377+
312378
@app.route('/')
313379
def index():
314380
"""Simple index page showing available API endpoints."""
@@ -1090,6 +1156,15 @@ def register():
10901156
try:
10911157
data = request.get_json()
10921158

1159+
# =========================================================================
1160+
# SECURITY AUDIT: REGISTRATION ATTEMPT
1161+
# =========================================================================
1162+
# All registration attempts are logged for security auditing purposes.
1163+
# CSRF protection is enforced automatically by Flask-WTF for this
1164+
# POST request, ensuring the signup originates from our own UI.
1165+
# =========================================================================
1166+
logger.info(f"Registration attempt for user: {data.get('username')} from IP: {request.remote_addr}")
1167+
10931168
is_valid, validated_data = validate_request(RegisterRequest, data)
10941169
if not is_valid:
10951170
return jsonify(validated_data), 400
@@ -1134,6 +1209,15 @@ def login():
11341209
try:
11351210
data = request.get_json()
11361211

1212+
# =========================================================================
1213+
# SECURITY AUDIT: LOGIN ATTEMPT
1214+
# =========================================================================
1215+
# All login attempts are strictly validated against CSRF tokens.
1216+
# This prevents an attacker from creating a malicious site that
1217+
# automatically logs a user into an account they control.
1218+
# =========================================================================
1219+
logger.info(f"Login attempt for identifier: {data.get('username')} from IP: {request.remote_addr}")
1220+
11371221
is_valid, validated_data = validate_request(LoginRequest, data)
11381222
if not is_valid:
11391223
return jsonify(validated_data), 400
@@ -1169,6 +1253,22 @@ def logout():
11691253
return resp, status
11701254

11711255

1256+
@app.route('/api/v1/auth/verify', methods=['GET'])
1257+
@jwt_required()
1258+
def verify_auth_session():
1259+
"""Validate JWT from access cookie and return the current user (session restore)."""
1260+
try:
1261+
uid = get_jwt_identity()
1262+
user = User.query.get(int(uid))
1263+
if not user:
1264+
return jsonify({"error": "User not found"}), 404
1265+
return jsonify({
1266+
"user": {"id": user.id, "username": user.username, "email": user.email}
1267+
}), 200
1268+
except (TypeError, ValueError):
1269+
return jsonify({"error": "Invalid session"}), 401
1270+
1271+
11721272
# ==================== READING STATS ENDPOINTS ====================
11731273
@app.route('/api/v1/stats/goal', methods=['POST'])
11741274
@jwt_required()

0 commit comments

Comments
 (0)